feat: index based access to the NGINX variables - #124
Conversation
`ngx.var.foo` resolves the name on every read: lua-nginx-module lowercases the name into a fresh r->pool allocation, hashes it and looks it up in cmcf->variables_hash before nginx dispatches to ngx_http_get_indexed_variable() anyway. Measured on a request-scoped benchmark that is ~23-33ns per read, against ~6ns for the FFI fast paths lua-var-nginx-module already provides for a dozen variables. Expose the index that nginx assigned at configuration time so the Lua side can skip straight to ngx_http_get_indexed_variable(): - `ngx_http_apisix_ffi_var_load_indexes()` enumerates cmcf->variables - `ngx_http_apisix_ffi_var_get_by_index()` / `..._set_by_index()` read and write r->variables[index], mirroring ngx_http_lua_ffi_var_set() semantics - `resty.apisix.var` builds the name to index map and falls back to ngx.var for anything the configuration did not index - `apisix_var_index $name ...` marks extra variables as indexed Only variables already present in cmcf->variables are exposed by default, and nginx resolves those through ngx_http_get_indexed_variable() itself, so reading them by index is semantically identical to reading them by name. Indexing a variable that was not indexed before does change its caching behaviour, which is why `apisix_var_index` is opt-in and documented.
📝 WalkthroughWalkthroughThis change adds indexed NGINX variable configuration, C FFI operations, Lua get/set APIs, fallback behavior for unindexed variables, documentation, and integration tests. ChangesIndexed variable access
Estimated code review effort: 4 (Complex) | ~45 minutes 🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
The previous restriction rested on a wrong claim: ngx_cycle was said to still point at the previous cycle while init_by_lua runs. Verified against the built runtime instead -- enumerating cmcf->variables from init_by_lua yields the current cycle's variables, on a reload too (10 of 10 values matched ngx.var before the reload, 16 of 16 after a reload that changed the indexed set). Drop the phase guard and record the real caveat: the init phase runs during postconfiguration, so a variable indexed by a module whose postconfiguration is ordered after ngx_http_lua_module's is not in cmcf->variables yet. It just stays on the ngx.var path, and cmcf->variables is append only, so the indexes handed out stay valid. TEST 8 now covers init phase loading.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 45-49: Update the README wording to use “name-to-index map” and
replace “from the init phase on, across a reload included” with “from the init
phase onward, including across reloads,” without changing the surrounding
initialization guidance.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7c215d77-6a6f-4455-85bb-b7091afbef9e
📒 Files selected for processing (4)
README.mdlib/resty/apisix/var.luasrc/ngx_http_apisix_var.ct/var-index.t
🚧 Files skipped from review as they are similar to previous changes (2)
- src/ngx_http_apisix_var.c
- lib/resty/apisix/var.lua
| `load_indexes()` builds the name to index map from `cmcf->variables`. It is safe | ||
| from the `init` phase on, across a reload included. Prefer `init_worker` when a | ||
| complete map matters: the `init` phase runs during postconfiguration, so a | ||
| variable that a module indexes from a later postconfiguration hook is not in | ||
| `cmcf->variables` yet and simply stays on the `ngx.var` path. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the initialization timing wording.
Use name-to-index map. Replace “from the init phase on, across a reload included” with “from the init phase onward, including across reloads.”
🧰 Tools
🪛 LanguageTool
[grammar] ~45-~45: Use a hyphen to join words.
Context: ... read. load_indexes() builds the name to index map from cmcf->variables. It is ...
(QB_NEW_EN_HYPHEN)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 45 - 49, Update the README wording to use
“name-to-index map” and replace “from the init phase on, across a reload
included” with “from the init phase onward, including across reloads,” without
changing the surrounding initialization guidance.
Source: Linters/SAST tools
What
Expose the variable index nginx assigns at configuration time so the Lua side can read and write NGINX variables through
ngx_http_get_indexed_variable()directly, instead of paying for the name resolutionngx.vardoes on every access.ngx_http_apisix_ffi_var_load_indexes()— enumeratecmcf->variablesngx_http_apisix_ffi_var_get_by_index()/..._set_by_index()resty.apisix.var— builds the name → index map, falls back tongx.varfor anything the configuration did not indexapisix_var_index $name ...;— opt-in directive to mark extra variables as indexedWhy
ngx.var.foogoes throughngx_http_lua_ffi_var_get(), which allocates a lowercase copy of the name fromr->pool, hashes it and looks it up incmcf->variables_hash— before nginx dispatches tongx_http_get_indexed_variable()anyway for any indexed variable.Measured inside the built runtime (openresty 1.29.2.4, x86-64), 1M iterations on a live request:
ngx.var.request_uriresty.apisix.var.get_by_index()For context on the size of the prize: an APISIX route with
prometheus+proxy-rewriteresolves 25 variables and writes 8 per request, so ~0.7 µs out of ~32 µs of worker CPU per request. This lands about half of that.Semantics
Reading an indexed variable by index is identical to reading it by name —
ngx_http_get_variable()already dispatches tongx_http_get_indexed_variable()for those variables,no_cacheableincluded. So the default set (whatever the configuration already indexed throughset, a log format,proxy_pass, ...) carries no behaviour change.apisix_var_indexis where behaviour can change and is therefore opt-in: nginx caches an indexed variable's value inr->variables[], while a variable that is only ever read throughngx.varis resolved on every access. Indexing$http_user_agentwould make a laterngx.req.set_header()invisible to readers of that variable. The README spells out which prefixes to keep out.Writing mirrors
ngx_http_lua_ffi_var_set():set_handlerfirst, thenr->variables[]for an indexed variable, otherwise rejected. Note thatngx_http_variables_init_vars()copies onlyget_handlerintocmcf->variables—set_handlerlives on thecmcf->variables_hashentry — so the index → hash-entry mapping is resolved once per cycle ininit_modulerather than hashing the name on every write.Tests
t/var-index.t, 11 cases: read by index and by name, fallback for a non-indexed variable, not-found, write to asetvariable, write through aset_handler($args), non-changeable variable, nil assignment, prefix variable read-but-not-writable,load_indexes()rejected in the init phase, and two configuration-error cases for the directive.Summary by CodeRabbit
New Features
resty.apisix.varlibrary.apisix_var_indexconfiguration directive for registering variables.Documentation