说明定时器回调无法捕获 upvalue 的原因和替代方案。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
222 lines
6.5 KiB
Markdown
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 |