Skip to content

feat: index based access to the NGINX variables - #124

Open
AlinsRan wants to merge 2 commits into
mainfrom
feat/var-index-access
Open

feat: index based access to the NGINX variables#124
AlinsRan wants to merge 2 commits into
mainfrom
feat/var-index-access

Conversation

@AlinsRan

@AlinsRan AlinsRan commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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 resolution ngx.var does on every access.

  • ngx_http_apisix_ffi_var_load_indexes() — enumerate cmcf->variables
  • ngx_http_apisix_ffi_var_get_by_index() / ..._set_by_index()
  • resty.apisix.var — builds the name → index map, falls back to ngx.var for anything the configuration did not index
  • apisix_var_index $name ...; — opt-in directive to mark extra variables as indexed

Why

ngx.var.foo goes through ngx_http_lua_ffi_var_get(), which allocates a lowercase copy of the name from r->pool, hashes it and looks it up in cmcf->variables_hash — before nginx dispatches to ngx_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:

access ns/op
ngx.var.request_uri 27.2
resty.apisix.var.get_by_index() 7.2

For context on the size of the prize: an APISIX route with prometheus + proxy-rewrite resolves 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 to ngx_http_get_indexed_variable() for those variables, no_cacheable included. So the default set (whatever the configuration already indexed through set, a log format, proxy_pass, ...) carries no behaviour change.

apisix_var_index is where behaviour can change and is therefore opt-in: nginx caches an indexed variable's value in r->variables[], while a variable that is only ever read through ngx.var is resolved on every access. Indexing $http_user_agent would make a later ngx.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_handler first, then r->variables[] for an indexed variable, otherwise rejected. Note that ngx_http_variables_init_vars() copies only get_handler into cmcf->variablesset_handler lives on the cmcf->variables_hash entry — so the index → hash-entry mapping is resolved once per cycle in init_module rather 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 a set variable, write through a set_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

    • Added indexed NGINX variable access through the resty.apisix.var library.
    • Added the apisix_var_index configuration directive for registering variables.
    • Supports reading and updating indexed variables, with fallback access for unindexed variables.
    • Added validation and clear errors for unsupported or read-only variable operations.
  • Documentation

    • Documented configuration, APIs, initialization timing, fallback behavior, write semantics, and caching considerations.

`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.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds indexed NGINX variable configuration, C FFI operations, Lua get/set APIs, fallback behavior for unindexed variables, documentation, and integration tests.

Changes

Indexed variable access

Layer / File(s) Summary
Configuration and module lifecycle
config, src/ngx_http_apisix_module.c, src/ngx_http_apisix_module.h, src/ngx_http_apisix_var.c
The module registers apisix_var_index, compiles the variable source, declares the FFI entry points, and initializes per-cycle variable state.
Indexed FFI operations
src/ngx_http_apisix_var.c
C APIs enumerate configured indexes and read or write indexed variables with validation, handler support, unset handling, allocation, and error reporting.
Lua variable API
lib/resty/apisix/var.lua, README.md
resty.apisix.var loads indexes and exposes indexed and name-based get/set functions. The README documents the directive and the Lua API. Name-based access falls back to ngx.var when no index exists.
Behavior validation
t/var-index.t
Tests cover reads, fallback, writes, set handlers, unset values, init-phase loading, prefix variables, and directive validation.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
E2e Test Quality Review ⚠️ Warning The 11 tests are real, readable NGINX E2E cases, but they omit core scenarios: non-indexed set() fallback, direct set_by_index(), cache changes after request mutation, empty values, and invalid ind... Add independent E2E cases for fallback writes, direct indexed APIs, empty and invalid inputs, and indexed-versus-fallback behavior after ngx.req.set_header().
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding index-based access to NGINX variables.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Check ✅ Passed Changed scope adds trusted Lua/NGINX variable access; it has no secret logging or persistence, endpoints, ownership, TLS, or secret-reference logic, and writes enforce NGINX changeability.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/var-index-access

Comment @coderabbitai help to get the list of available commands.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4c644ea and 780598c.

📒 Files selected for processing (4)
  • README.md
  • lib/resty/apisix/var.lua
  • src/ngx_http_apisix_var.c
  • t/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

Comment thread README.md
Comment on lines +45 to +49
`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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant