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

222 lines
6.5 KiB
Markdown

# 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:
```lua
-- 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
```go
// In Go code
engine.CreateSharedDict("my_dict", 1000) // max 1000 items
```
### Lua Usage
```lua
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
```lua
-- 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
```lua
local timer, err = ngx.timer.at(10, callback)
if timer then
timer:cancel() -- Cancel before it fires
end
```
## Subrequest API
### ngx.location.capture
```lua
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:
```lua
-- 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