lolly/docs/lua/API.md
xfy c4adcfa76a docs(lua): 添加定时器回调限制说明文档
说明定时器回调无法捕获 upvalue 的原因和替代方案。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:24:43 +08:00

6.5 KiB

Lua Engine API Reference

Timer Callback Limitations

No Upvalue Capture

Timer callbacks cannot capture local variables (upvalues/closure variables). Attempting to register a callback with captured variables will fail with:

timer callback cannot capture upvalues (closure variables); use shared dict instead

Reason: The timer callback executes in a dedicated scheduler LState after the source coroutine has died. Captured values would reference dead coroutine memory.

Workaround: Use shared dict to pass data:

-- WRONG: captures local variable
local request_id = ngx.var.request_id
ngx.timer.at(5, function()
    ngx.log(ngx.INFO, request_id)  -- sees nil, not captured value
end)

-- RIGHT: use shared dict
local dict = ngx.shared.timer_data
dict:set("request_id", ngx.var.request_id)
ngx.timer.at(5, function()
    local dict = ngx.shared.timer_data
    ngx.log(ngx.INFO, dict:get("request_id"))
end)

Safe vs Unsafe API Scope

Safe APIs (available in timer callbacks)

These APIs can be called from timer callbacks without restriction:

API Description
ngx.shared.DICT.* Shared memory operations (get/set/add/incr/delete/flush)
ngx.log(level, ...) Logging without request context
ngx.timer.at() Create new timers (nesting supported)
ngx.timer.running_count() Get active timer count

Unsafe APIs (blocked in timer callbacks)

These APIs require request context and raise errors when called in timer callbacks:

API Error Message
ngx.req.* API ngx.req.X not available in timer callback context
ngx.resp.* API ngx.resp.X not available in timer callback context
ngx.var.* API ngx.var not available in timer callback context
ngx.ctx.* API ngx.ctx not available in timer callback context
ngx.location.capture() API ngx.location.capture not available in timer callback context
ngx.say/print/flush API ngx.say not available in timer callback context
ngx.exit/redirect API ngx.exit not available in timer callback context

Shared Dictionary API

Creating Shared Dicts

// In Go code
engine.CreateSharedDict("my_dict", 1000)  // max 1000 items

Lua Usage

local dict = ngx.shared.my_dict

-- Basic operations
dict:set("key", "value", 3600)  -- TTL: 3600 seconds
dict:add("key", "value", 3600)  -- Add only if not exists
dict:replace("key", "value", 3600)  -- Replace only if exists

local value = dict:get("key")
local value, err = dict:get_stale("key")  -- Get expired items

dict:delete("key")

-- Increment
local new_val, err = dict:incr("counter", 1)
local new_val, err = dict:incr("counter", 1, 0)  -- Init value if not exists

-- Flush all
local count = dict:flush_all()
local count = dict:flush_expired(100)  -- Flush expired, max 100

Timer API

Creating Timers

-- Basic timer
local ok, err = ngx.timer.at(5, function()
    ngx.log(ngx.INFO, "timer fired")
end)

-- With arguments
local ok, err = ngx.timer.at(5, function(premature, arg1, arg2)
    ngx.log(ngx.INFO, "timer args:", arg1, arg2)
end, "value1", "value2")

-- Check running count
local count = ngx.timer.running_count()

Canceling Timers

local timer, err = ngx.timer.at(10, callback)
if timer then
    timer:cancel()  -- Cancel before it fires
end

Subrequest API

ngx.location.capture

local res = ngx.location.capture("/internal/path")

-- Result structure
-- res.status: HTTP status code
-- res.body: Response body
-- res.headers: Response headers table

-- With options
local res = ngx.location.capture("/api", {
    method = "POST",
    body = '{"data": "value"}',
    headers = {
        ["Content-Type"] = "application/json"
    }
})

Best Practices

Timer Callbacks

  1. Always use shared dict for passing data to timer callbacks
  2. Keep callbacks short - avoid long-running operations
  3. Handle errors gracefully - use pcall for error handling
  4. Avoid blocking operations - no network calls in timer context (use cosocket in request context instead)

Shared Dictionary

  1. Set appropriate TTLs - avoid memory leaks from stale data
  2. Use atomic operations - add, replace, incr for concurrent safety
  3. Handle errors - check return values for err

Performance

  1. Minimize timer count - use running_count() to monitor
  2. Batch operations - use flush_expired periodically
  3. Reuse shared dicts - create once at engine startup

Thread Safety

The Lua engine uses a dedicated scheduler goroutine for timer callback execution. All LState operations are single-threaded:

  • Request handlers execute in their own goroutine with per-request LState
  • Timer callbacks execute in the scheduler goroutine with dedicated LState
  • Shared dicts are thread-safe and can be accessed from both contexts

Graceful Shutdown

On engine close:

  1. New timers are rejected (stopping flag)
  2. The callback queue channel is closed
  3. The scheduler goroutine drains remaining callbacks
  4. A 5-second timeout is enforced
  5. Remaining callbacks are abandoned and logged

Example shutdown log:

[lua] shutdown timeout: 3 callbacks abandoned

Known Limitations

Limitation Reason Workaround
Timer callbacks cannot capture upvalues gopher-lua NewFunctionFromProto does not preserve upvalue closures Use ngx.shared.DICT to pass data
Request-scoped APIs unavailable in timer callbacks Timer callbacks have no RequestCtx Use shared dict for data sharing
Subrequests use deep-copied parent request data Cannot access coroutine's live RequestCtx from API function Parent data is copied at capture time

Troubleshooting

"timer callback cannot capture upvalues"

Your callback captured a local variable. Use shared dict instead:

-- Instead of:
local data = ngx.var.request_id
ngx.timer.at(5, function() ngx.log(ngx.INFO, data) end)

-- Use:
ngx.shared.timer_data:set("key", ngx.var.request_id)
ngx.timer.at(5, function()
    ngx.log(ngx.INFO, ngx.shared.timer_data:get("key"))
end)

"API ngx.X not available in timer callback context"

The API requires request context. Timer callbacks run in an isolated scheduler LState. Restructure your code to pass needed data via shared dict before creating the timer.

High timer callback latency

  • Check callback queue depth (default: 1024)
  • Long-running callbacks delay subsequent ones
  • Consider reducing callback work or splitting into multiple timers