lolly/internal/lua/api_shared_dict_test.go
xfy 6c7cf73c87 refactor(lua): replace single LState with LState pool architecture
Replace the single LState + coroutine model with an LState pool to
eliminate concurrent map read/write issues in gopher-lua. Each request
now gets a completely independent LState with its own Global table.

Key changes:
- Add LStatePool for managing pooled LState instances
- Remove shared Engine.L and coroutine-based execution
- Simplify coroutine.go: remove yield handling, use direct PCall
- Remove ngxRegisterMu lock (no longer needed with isolated LStates)
- Update config.go: add LStatePoolInitialSize/MaxSize settings
- Update tests to work with the new architecture

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 10:38:10 +08:00

544 lines
15 KiB
Go
Raw Permalink 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 提供 ngx.shared API 扩展测试,覆盖 Lua API 函数的更多场景
package lua
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestSharedDictLuaReplace 测试 dict:replace
func TestSharedDictLuaReplace(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("testdict", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
// 测试 replace 存在的 key
err = L.DoString(`
local dict = ngx.shared.DICT("testdict")
-- 先设置一个值
dict:set("mykey", "old_value")
-- replace 存在的 key 应该成功
local ok, err = dict:replace("mykey", "new_value")
assert(ok == true, "replace existing key should succeed: " .. tostring(err))
-- 验证值已更新
local val, _ = dict:get("mykey")
assert(val == "new_value", "value should be updated")
`)
require.NoError(t, err)
// 测试 replace 不存在的 key
err = L.DoString(`
local dict = ngx.shared.DICT("testdict")
-- replace 不存在的 key 应该失败
local ok, err = dict:replace("nonexistent", "value")
assert(ok == false, "replace nonexistent key should fail")
assert(err == "not found", "error should be 'not found', got: " .. tostring(err))
`)
require.NoError(t, err)
}
// TestSharedDictLuaReplaceWithTTL 测试 dict:replace 带 TTL
func TestSharedDictLuaReplaceWithTTL(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("ttldict", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
err = L.DoString(`
local dict = ngx.shared.DICT("ttldict")
dict:set("ttlkey", "original")
-- replace 带 TTL0.1 秒)
local ok, err = dict:replace("ttlkey", "replaced", 0.1)
assert(ok == true, "replace with TTL should succeed: " .. tostring(err))
`)
require.NoError(t, err)
// 等待过期
time.Sleep(150 * time.Millisecond)
// 验证过期
err = L.DoString(`
local dict = ngx.shared.DICT("ttldict")
local val, err = dict:get("ttlkey")
assert(val == nil, "expired key should return nil")
`)
require.NoError(t, err)
}
// TestSharedDictLuaIndexAccess 测试 dict["key"] 索引访问
func TestSharedDictLuaIndexAccess(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("idxdict", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
err = L.DoString(`
local dict = ngx.shared.DICT("idxdict")
-- 通过方法设置值
dict:set("foo", "bar")
-- 通过索引方式读取
local val = dict["foo"]
assert(val == "bar", "index access should work")
-- 通过 __newindex 设置值
dict["newkey"] = "newvalue"
local v = dict:get("newkey")
assert(v == "newvalue", "__newindex should work")
`)
require.NoError(t, err)
}
// TestSharedDictLuaIndexNotFound 测试索引访问不存在的 key
func TestSharedDictLuaIndexNotFound(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("idxdict2", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
err = L.DoString(`
local dict = ngx.shared.DICT("idxdict2")
-- 读取不存在的 key 应该返回 nil
local val = dict["nonexistent"]
assert(val == nil, "nonexistent key should return nil")
`)
require.NoError(t, err)
}
// TestSharedDictLuaIndexMethodAccess 测试索引访问方法名
func TestSharedDictLuaIndexMethodAccess(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("metdict", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
err = L.DoString(`
local dict = ngx.shared.DICT("metdict")
-- 通过索引访问方法
local getFn = dict["get"]
assert(type(getFn) == "function", "get should be a function")
local setFn = dict["set"]
assert(type(setFn) == "function", "set should be a function")
`)
require.NoError(t, err)
}
// TestSharedDictLuaFlushExpired 测试 dict:flush_expired
func TestSharedDictLuaFlushExpired(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("flushdict", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
// 设置一些带 TTL 的条目和一个永久的
err = L.DoString(`
local dict = ngx.shared.DICT("flushdict")
dict:set("exp1", "v1", 0.05)
dict:set("exp2", "v2", 0.05)
dict:set("perm", "permanent")
`)
require.NoError(t, err)
// 立即清除应该没有过期条目
err = L.DoString(`
local dict = ngx.shared.DICT("flushdict")
local count = dict:flush_expired()
assert(count == 0, "should have 0 expired immediately")
`)
require.NoError(t, err)
// 等待过期
time.Sleep(100 * time.Millisecond)
// 现在应该有 2 个过期条目
err = L.DoString(`
local dict = ngx.shared.DICT("flushdict")
local count = dict:flush_expired()
assert(count == 2, "should have 2 expired after wait")
-- 永久条目应该还在
local val, _ = dict:get("perm")
assert(val == "permanent", "permanent key should still exist")
`)
require.NoError(t, err)
}
// TestSharedDictLuaGetKeys 测试 dict:get_keys
func TestSharedDictLuaGetKeys(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("keysdict", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
err = L.DoString(`
local dict = ngx.shared.DICT("keysdict")
dict:set("k1", "v1")
dict:set("k2", "v2")
dict:set("k3", "v3")
local keys = dict:get_keys()
assert(type(keys) == "table", "get_keys should return a table")
-- 当前实现返回空表
`)
require.NoError(t, err)
}
// TestSharedDictLuaSize 测试 dict:size
func TestSharedDictLuaSize(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("sizedict", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
err = L.DoString(`
local dict = ngx.shared.DICT("sizedict")
-- 初始为 0
assert(dict:size() == 0, "initial size should be 0")
dict:set("a", "1")
dict:set("b", "2")
dict:set("c", "3")
assert(dict:size() >= 3, "size should be at least 3")
`)
require.NoError(t, err)
}
// TestSharedDictLuaFreeSpace 测试 dict:free_space
func TestSharedDictLuaFreeSpace(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("freedict", 50)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
err = L.DoString(`
local dict = ngx.shared.DICT("freedict")
-- 初始 free_space 应该等于 maxItems
local free = dict:free_space()
assert(free == 50, "initial free_space should be 50, got: " .. tostring(free))
-- 添加条目后 free_space 减少
dict:set("key1", "value1")
local free2 = dict:free_space()
assert(free2 == 49, "free_space should be 49 after 1 item, got: " .. tostring(free2))
`)
require.NoError(t, err)
}
// TestSharedDictLuaFlushAll 测试 dict:flush_all
func TestSharedDictLuaFlushAll(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("flushalldict", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
err = L.DoString(`
local dict = ngx.shared.DICT("flushalldict")
dict:set("a", "1")
dict:set("b", "2")
assert(dict:size() >= 2)
-- flush_all 清空所有
dict:flush_all()
assert(dict:size() == 0, "size should be 0 after flush_all")
`)
require.NoError(t, err)
}
// TestSharedDictLuaDictNotFound 测试请求不存在的 shared dict
func TestSharedDictLuaDictNotFound(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
err = L.DoString(`
local dict, err = ngx.shared.DICT("nonexistent_dict")
assert(dict == nil, "nonexistent dict should return nil")
assert(err ~= nil, "should return error message")
`)
require.NoError(t, err)
}
// TestSharedDictLuaToString 测试 dict 的 tostring
func TestSharedDictLuaToString(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("tostringdict", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
err = L.DoString(`
local dict = ngx.shared.DICT("tostringdict")
local s = tostring(dict)
assert(type(s) == "string", "tostring should return a string")
assert(string.match(s, "ngx.shared.dict"), "should contain 'ngx.shared.dict'")
`)
require.NoError(t, err)
}
// TestSharedDictLuaIncrNonNumber 测试 incr 对非数值
func TestSharedDictLuaIncrNonNumber(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("incrdict", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
// 设置一个非数值
dict := engine.SharedDictManager().GetDict("incrdict")
dict.Set("notnum", "hello", 0)
err = L.DoString(`
local dict = ngx.shared.DICT("incrdict")
local val, err = dict:incr("notnum", 1)
-- 非数值 incr 返回 nil
assert(val == nil, "incr non-number should return nil")
`)
require.NoError(t, err)
}
// TestSharedDictLuaAddMultipleKeys 测试批量添加
func TestSharedDictLuaAddMultipleKeys(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("multiadd", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
err = L.DoString(`
local dict = ngx.shared.DICT("multiadd")
-- 批量添加
for i = 1, 10 do
local ok, err = dict:add("key_" .. i, "val_" .. i)
assert(ok == true, "add should succeed: " .. tostring(err))
end
-- 验证所有 key 都存在
for i = 1, 10 do
local val, _ = dict:get("key_" .. i)
assert(val == "val_" .. i, "key_" .. i .. " should have correct value")
end
assert(dict:size() == 10, "size should be 10")
`)
require.NoError(t, err)
}
// TestSharedDictEngineAPI 测试通过 Engine API 创建共享字典
func TestSharedDictEngineAPI(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
// 通过 Engine 的 CreateSharedDict 方法创建
dict := engine.CreateSharedDict("enginedict", 50)
require.NotNil(t, dict)
// 验证可以通过 SharedDictManager 获取
dict2 := engine.SharedDictManager().GetDict("enginedict")
assert.Equal(t, dict, dict2, "should return same dict")
// 再次调用 CreateSharedDict 应返回已存在的
dict3 := engine.CreateSharedDict("enginedict", 100)
assert.Equal(t, dict, dict3, "should return existing dict regardless of maxItems")
}
// TestSharedDictLuaSetWithTTL 测试 set 带 TTL
func TestSharedDictLuaSetWithTTL(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("setttldict", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
err = L.DoString(`
local dict = ngx.shared.DICT("setttldict")
-- set 带 TTL0.1 秒)
local ok, err = dict:set("ttlkey", "temp_value", 0.1)
assert(ok == true, "set with TTL should succeed: " .. tostring(err))
-- 立即获取应该成功
local val, exp = dict:get("ttlkey")
assert(val == "temp_value", "value should be correct")
assert(exp == 0 or exp == nil, "should not be expired yet")
`)
require.NoError(t, err)
// 等待过期
time.Sleep(150 * time.Millisecond)
err = L.DoString(`
local dict = ngx.shared.DICT("setttldict")
local val, err = dict:get("ttlkey")
assert(val == nil, "expired key should return nil")
assert(err == "expired", "error should be 'expired', got: " .. tostring(err))
`)
require.NoError(t, err)
}
// TestSharedDictLuaAddWithTTL 测试 add 带 TTL
func TestSharedDictLuaAddWithTTL(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("addttldict", 100)
L := engine.GetLStateForTest()
defer engine.PutLStateForTest(L)
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
err = L.DoString(`
local dict = ngx.shared.DICT("addttldict")
-- add 带 TTL
local ok, err = dict:add("addkey", "temp", 0.1)
assert(ok == true, "add with TTL should succeed: " .. tostring(err))
`)
require.NoError(t, err)
time.Sleep(150 * time.Millisecond)
err = L.DoString(`
local dict = ngx.shared.DICT("addttldict")
local val, err = dict:get("addkey")
assert(val == nil, "expired key should return nil")
`)
require.NoError(t, err)
}
// TestSharedDictClose 测试 SharedDictManager.Close
func TestSharedDictClose(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
_ = engine.CreateSharedDict("closedict", 100)
// Close 引擎会清理 sharedDictManager
engine.Close()
// Close 后不应该 panic即使再次 Close
engine.Close()
}