From aa7346beae624cd7c9b7b16d76ecd51d907600af Mon Sep 17 00:00:00 2001 From: An Tran Date: Fri, 16 Jan 2026 18:06:12 +1000 Subject: [PATCH 1/5] perf: cache policy manifests Previously, every time the policy chain was rebuilt, the gateway would look for the manifests file on the disk. This caused a delay for incoming requests. Since the manifests are static and do not change at runtime, it is more efficient to cache them at the module level. This caching will speed up the lookup process. --- gateway/src/apicast/policy_loader.lua | 40 ++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/gateway/src/apicast/policy_loader.lua b/gateway/src/apicast/policy_loader.lua index 2ccecf2cc..34554623c 100644 --- a/gateway/src/apicast/policy_loader.lua +++ b/gateway/src/apicast/policy_loader.lua @@ -16,6 +16,11 @@ local concat = table.concat local setmetatable = setmetatable local pcall = pcall +local tab_new = require('table.new') +local isempty = require('table.isempty') + +local manifests_cache = tab_new(32, 0) + local _M = {} local resty_env = require('resty.env') @@ -70,8 +75,22 @@ local function lua_load_path(load_path) return format('%s/?.lua', load_path) end +local function get_manifest(name, version) + local manifests = manifests_cache[name] + if manifests then + for _, manifest in ipairs(manifests) do + if version == manifest.version then + return manifest + end + end + end +end + local function load_manifest(name, version, path) - local manifest = read_manifest(path) + local manifest = get_manifest(name, version) + if not manifest then + manifest = read_manifest(path) + end if manifest then if manifest.version ~= version then @@ -110,8 +129,8 @@ end function _M:load_path(name, version, paths) local failures = {} - for _, path in ipairs(paths or self.policy_load_paths()) do - local manifest, load_path = load_manifest(name, version, format('%s/%s/%s', path, name, version) ) + if version == 'builtin' then + local manifest, load_path = load_manifest(name, version, format('%s/%s', self.builtin_policy_load_path(), name) ) if manifest then return load_path, manifest.configuration @@ -120,8 +139,8 @@ function _M:load_path(name, version, paths) end end - if version == 'builtin' then - local manifest, load_path = load_manifest(name, version, format('%s/%s', self.builtin_policy_load_path(), name) ) + for _, path in ipairs(paths or self.policy_load_paths()) do + local manifest, load_path = load_manifest(name, version, format('%s/%s/%s', path, name, version) ) if manifest then return load_path, manifest.configuration @@ -130,6 +149,7 @@ function _M:load_path(name, version, paths) end end + return nil, nil, failures end @@ -173,9 +193,15 @@ end -- Returns all the policy modules function _M:get_all() local policy_modules = {} + local manifests - local policy_manifests_loader = require('apicast.policy_manifests_loader') - local manifests = policy_manifests_loader.get_all() + if isempty(manifests_cache) then + local policy_manifests_loader = require('apicast.policy_manifests_loader') + manifests = policy_manifests_loader.get_all() + manifests_cache = manifests + else + manifests = manifests_cache + end for policy_name, policy_manifests in pairs(manifests) do for _, manifest in ipairs(policy_manifests) do From ad0342d1a8915a3c0bea3abbe7b1bf144f01a5ea Mon Sep 17 00:00:00 2001 From: An Tran Date: Fri, 16 Jan 2026 19:04:33 +1000 Subject: [PATCH 2/5] perf: reuse PolicyOrderChecker --- gateway/src/apicast/policy_chain.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gateway/src/apicast/policy_chain.lua b/gateway/src/apicast/policy_chain.lua index f8b6d07a2..1c1d089e7 100644 --- a/gateway/src/apicast/policy_chain.lua +++ b/gateway/src/apicast/policy_chain.lua @@ -193,12 +193,15 @@ function _M:add_policy(name, version, ...) end end +local default_policy_order_check = PolicyOrderChecker.new(policy_manifests_loader.get_all()) + -- Checks if there are any policies placed in the wrong place in the chain. -- It doesn't return anything, it prints error messages when there's a problem. function _M:check_order(manifests) - PolicyOrderChecker.new( - manifests or policy_manifests_loader.get_all() - ):check(self) + if manifests then + PolicyOrderChecker.new(manifests):check(self) + end + default_policy_order_check:check(self) end local function call_chain(phase_name) From 771ea2677f831c2d9b9e83ae78b46fd5a3187d0e Mon Sep 17 00:00:00 2001 From: An Tran Date: Fri, 16 Jan 2026 21:20:29 +1000 Subject: [PATCH 3/5] perf: cache the JSON schema validator Creating a json schema validator is somewhat expensive, so we cache this step with a local LRU cache --- .../src/apicast/policy_config_validator.lua | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/gateway/src/apicast/policy_config_validator.lua b/gateway/src/apicast/policy_config_validator.lua index dbc9eb0da..46a61460f 100644 --- a/gateway/src/apicast/policy_config_validator.lua +++ b/gateway/src/apicast/policy_config_validator.lua @@ -3,8 +3,35 @@ -- Validates a policy configuration against a policy config JSON schema. local jsonschema = require('jsonschema') +local lrucache = require("resty.lrucache") -local _M = { } +local cached_validator = lrucache.new(100) + +local _M = { + _VERSION=0.1 +} + +local function create_validator(schema) + local ok, res = pcall(jsonschema.generate_validator, schema) + if ok then + return res + end + + return nil, res +end + +local function get_validator(schema) + local validator, err = cached_validator:get(schema) + if not validator then + validator, err = create_validator(schema) + if not validator then + return nil, err + end + cached_validator:set(schema, validator) + end + + return validator, nil +end --- Validate a policy configuration -- Checks if a policy configuration is valid according to the given schema. @@ -13,7 +40,10 @@ local _M = { } -- @treturn boolean True if the policy configuration is valid. False otherwise. -- @treturn string Error message only when the policy config is invalid. function _M.validate_config(config, config_schema) - local validator = jsonschema.generate_validator(config_schema or {}) + local validator, err = get_validator(config_schema or {}) + if not validator then + return false, err + end return validator(config or {}) end From 2a13c8e2cbfffb8d846966a25fd805880451b217 Mon Sep 17 00:00:00 2001 From: An Tran Date: Mon, 19 Jan 2026 13:18:21 +1000 Subject: [PATCH 4/5] perf: avoid construct the full policy chain when parsing response from portal --- .../configuration_loader/remote_v2.lua | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/gateway/src/apicast/configuration_loader/remote_v2.lua b/gateway/src/apicast/configuration_loader/remote_v2.lua index a86f2b47b..4bbe36565 100644 --- a/gateway/src/apicast/configuration_loader/remote_v2.lua +++ b/gateway/src/apicast/configuration_loader/remote_v2.lua @@ -102,22 +102,24 @@ local function service_config_endpoint(portal_endpoint, service_id, env, version ) end +local function get_oidc_issuer_endpoint(proxy_content) + return proxy_content.proxy and proxy_content.proxy.oidc_issuer_endpoint +end + local function parse_proxy_configs(self, proxy_configs) local config = { services = array(), oidc = array() } for i, proxy_conf in ipairs(proxy_configs) do local proxy_config = proxy_conf.proxy_config + local content = proxy_config.content - -- Copy the config because parse_service have side-effects. It adds - -- liquid templates in some policies and those cannot be encoded into a - -- JSON. We should get rid of these side effects. - local original_proxy_config = deepcopy(proxy_config) + config.services[i] = content - local service = configuration.parse_service(proxy_config.content) - - -- We always assign a oidc to the service, even an empty one with the - -- service_id, if not on APICAST_SERVICES_LIST will fail on filtering - local oidc = self:oidc_issuer_configuration(service) + local issuer_endpoint = get_oidc_issuer_endpoint(content) + local oidc + if issuer_endpoint then + oidc = self.oidc:call(issuer_endpoint, self.ttl) + end if not oidc then oidc = {} end @@ -125,10 +127,9 @@ local function parse_proxy_configs(self, proxy_configs) -- deepcopy because this can be cached, and we want to have a deepcopy to -- avoid issues with service_id local oidc_copy = deepcopy(oidc) - oidc_copy.service_id = service.id + oidc_copy.service_id = tostring(content.id) config.oidc[i] = oidc_copy - config.services[i] = original_proxy_config.content end return cjson.encode(config) end @@ -482,20 +483,22 @@ function _M:config(service, environment, version, service_regexp_filter) if res.status == 200 then local proxy_config = cjson.decode(res.body).proxy_config - - -- Copy the config because parse_service have side-effects. It adds - -- liquid templates in some policies and those cannot be encoded into a - -- JSON. We should get rid of these side effects. - local original_proxy_config = deepcopy(proxy_config) + local content = proxy_config.content local config_service = configuration.parse_service(proxy_config.content) if service_regexp_filter and not config_service:match_host(service_regexp_filter) then return nil, "Service filtered out because APICAST_SERVICES_FILTER_BY_URL" end - original_proxy_config.oidc = self:oidc_issuer_configuration(config_service) + local issuer_endpoint = get_oidc_issuer_endpoint(content) + local oidc + + if issuer_endpoint then + oidc = self.oidc:call(issuer_endpoint, self.ttl) + end - return original_proxy_config + proxy_config.oidc = oidc + return proxy_config else return nil, status_code_error(res) end From e5d86296c39ac0be8790caec3d191feaaf88b27c Mon Sep 17 00:00:00 2001 From: An Tran Date: Thu, 22 Jan 2026 12:59:38 +1000 Subject: [PATCH 5/5] Address PR feedbacks --- .../src/apicast/configuration_loader/remote_v2.lua | 6 ++---- gateway/src/apicast/policy_chain.lua | 2 +- gateway/src/apicast/policy_loader.lua | 12 ++++++++---- spec/configuration_loader/remote_v2_spec.lua | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/gateway/src/apicast/configuration_loader/remote_v2.lua b/gateway/src/apicast/configuration_loader/remote_v2.lua index 4bbe36565..5ca66b087 100644 --- a/gateway/src/apicast/configuration_loader/remote_v2.lua +++ b/gateway/src/apicast/configuration_loader/remote_v2.lua @@ -120,6 +120,8 @@ local function parse_proxy_configs(self, proxy_configs) if issuer_endpoint then oidc = self.oidc:call(issuer_endpoint, self.ttl) end + -- We always assign a oidc to the service, even an empty one with the + -- service_id, if not on APICAST_SERVICES_LIST will fail on filtering if not oidc then oidc = {} end @@ -452,10 +454,6 @@ function _M:services() return services end -function _M:oidc_issuer_configuration(service) - return self.oidc:call(service.oidc.issuer_endpoint, self.ttl) -end - function _M:config(service, environment, version, service_regexp_filter) local http_client = self.http_client diff --git a/gateway/src/apicast/policy_chain.lua b/gateway/src/apicast/policy_chain.lua index 1c1d089e7..26c4f84b5 100644 --- a/gateway/src/apicast/policy_chain.lua +++ b/gateway/src/apicast/policy_chain.lua @@ -199,7 +199,7 @@ local default_policy_order_check = PolicyOrderChecker.new(policy_manifests_loade -- It doesn't return anything, it prints error messages when there's a problem. function _M:check_order(manifests) if manifests then - PolicyOrderChecker.new(manifests):check(self) + return PolicyOrderChecker.new(manifests):check(self) end default_policy_order_check:check(self) end diff --git a/gateway/src/apicast/policy_loader.lua b/gateway/src/apicast/policy_loader.lua index 34554623c..39855dc50 100644 --- a/gateway/src/apicast/policy_loader.lua +++ b/gateway/src/apicast/policy_loader.lua @@ -16,10 +16,10 @@ local concat = table.concat local setmetatable = setmetatable local pcall = pcall -local tab_new = require('table.new') local isempty = require('table.isempty') -local manifests_cache = tab_new(32, 0) +-- Module-level cache storage (one per worker process) +local manifests_cache = {} local _M = {} @@ -75,7 +75,11 @@ local function lua_load_path(load_path) return format('%s/?.lua', load_path) end -local function get_manifest(name, version) +-- Get a cached manifest by policy name and version +-- @tparam string name The policy name +-- @tparam string version The policy version +-- @treturn table|nil The cached manifest table, or nil if not cached +local function get_cached_manifest(name, version) local manifests = manifests_cache[name] if manifests then for _, manifest in ipairs(manifests) do @@ -87,7 +91,7 @@ local function get_manifest(name, version) end local function load_manifest(name, version, path) - local manifest = get_manifest(name, version) + local manifest = get_cached_manifest(name, version) if not manifest then manifest = read_manifest(path) end diff --git a/spec/configuration_loader/remote_v2_spec.lua b/spec/configuration_loader/remote_v2_spec.lua index 7854f9599..0eee62dd1 100644 --- a/spec/configuration_loader/remote_v2_spec.lua +++ b/spec/configuration_loader/remote_v2_spec.lua @@ -651,7 +651,7 @@ UwIDAQAB it('does not crash on empty issuer', function() local service = { oidc = { issuer_endpoint = '' }} - assert.falsy(loader:oidc_issuer_configuration(service)) + assert.falsy(loader.oidc:call(service.oidc.issuer_endpoint, 0)) end) end)