docs(lua): 添加定时器回调限制说明文档
说明定时器回调无法捕获 upvalue 的原因和替代方案。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7d53cc3dea
commit
c4adcfa76a
222
docs/lua/API.md
Normal file
222
docs/lua/API.md
Normal file
@ -0,0 +1,222 @@
|
||||
# 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
|
||||
Loading…
x
Reference in New Issue
Block a user