From 6a6f79e2b001f5493179a38c0fa8fcbb57bf8c6b 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 846a287dbbfef801d535c8e2ffb2cb589122f8d7 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 5cc52fe2546be8970fa869bb70ad0dd8d4739929 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 5d00a069eff8ee5b332ca6dcefc8a999f55fcc98 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 7305afa3d..dcb42382f 100644 --- a/gateway/src/apicast/configuration_loader/remote_v2.lua +++ b/gateway/src/apicast/configuration_loader/remote_v2.lua @@ -103,22 +103,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 @@ -126,10 +128,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 @@ -480,20 +481,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 f5dcd20d60eb16c4a75218025c1c16f4faed4fc7 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 dcb42382f..dc64ae461 100644 --- a/gateway/src/apicast/configuration_loader/remote_v2.lua +++ b/gateway/src/apicast/configuration_loader/remote_v2.lua @@ -121,6 +121,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 @@ -450,10 +452,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)