This is an automated email from the ASF dual-hosted git repository.
nic443 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 73618d48f refactor(improvement): use secret URI as key for cache and
refactor lrucache (#12682)
73618d48f is described below
commit 73618d48f32e263228f1a0a0df71fbc3ec5d9a54
Author: Ashish Tiwari <[email protected]>
AuthorDate: Thu Oct 23 15:29:57 2025 +0530
refactor(improvement): use secret URI as key for cache and refactor
lrucache (#12682)
---
apisix/cli/config.lua | 4 +-
apisix/core/lrucache.lua | 20 ++
apisix/plugins/ai-aws-content-moderation.lua | 2 +-
apisix/plugins/authz-keycloak.lua | 2 +-
apisix/plugins/limit-count.lua | 2 +-
apisix/plugins/openid-connect.lua | 2 +-
apisix/secret.lua | 120 ++++++------
apisix/ssl/router/radixtree_sni.lua | 2 +-
conf/config.yaml.example | 6 +-
t/config-center-json/secret.t | 11 +-
t/config-center-yaml/secret.t | 9 +-
t/core/lrucache2.t | 272 +++++++++++++++++++++++++++
12 files changed, 379 insertions(+), 73 deletions(-)
diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua
index 35212eea7..1dd9364f4 100644
--- a/apisix/cli/config.lua
+++ b/apisix/cli/config.lua
@@ -83,7 +83,9 @@ local _M = {
lru = {
secret = {
ttl = 300,
- count = 512
+ count = 512,
+ neg_ttl = 60,
+ neg_count = 512
}
}
},
diff --git a/apisix/core/lrucache.lua b/apisix/core/lrucache.lua
index 374d33c90..6999685ab 100644
--- a/apisix/core/lrucache.lua
+++ b/apisix/core/lrucache.lua
@@ -98,9 +98,23 @@ local function new_lru_fun(opts)
local refresh_stale = opts and opts.refresh_stale
local serial_creating = opts and opts.serial_creating
local lru_obj = lru_new(item_count)
+
+ local neg_lru_obj
+ if opts and opts.neg_ttl and opts.neg_count then
+ neg_lru_obj = lru_new(opts.neg_count)
+ end
+
stale_obj_pool[lru_obj] = {}
return function (key, version, create_obj_fun, ...)
+ -- check negative cache first
+ if neg_lru_obj then
+ local neg_obj = neg_lru_obj:get(key)
+ if neg_obj and neg_obj.ver == version then
+ return nil, neg_obj.err
+ end
+ end
+
if not serial_creating or not can_yield_phases[get_phase()] then
local cache_obj = fetch_valid_cache(lru_obj, invalid_stale,
refresh_stale,
item_ttl, key, version, create_obj_fun, ...)
@@ -111,6 +125,9 @@ local function new_lru_fun(opts)
local obj, err = create_obj_fun(...)
if obj ~= nil then
lru_obj:set(key, {val = obj, ver = version}, item_ttl)
+ elseif neg_lru_obj then
+ -- cache the failure in negative cache
+ neg_lru_obj:set(key, {err = err, ver = version}, opts.neg_ttl)
end
return obj, err
@@ -146,6 +163,9 @@ local function new_lru_fun(opts)
local obj, err = create_obj_fun(...)
if obj ~= nil then
lru_obj:set(key, {val = obj, ver = version}, item_ttl)
+ elseif neg_lru_obj then
+ -- cache the failure in negative cache
+ neg_lru_obj:set(key, {err = err, ver = version}, opts.neg_ttl)
end
lock:unlock()
log.info("unlock with key ", key_s)
diff --git a/apisix/plugins/ai-aws-content-moderation.lua
b/apisix/plugins/ai-aws-content-moderation.lua
index d229b47b2..2cf45d6d2 100644
--- a/apisix/plugins/ai-aws-content-moderation.lua
+++ b/apisix/plugins/ai-aws-content-moderation.lua
@@ -88,7 +88,7 @@ end
function _M.rewrite(conf, ctx)
- conf = fetch_secrets(conf, true, conf, "")
+ conf = fetch_secrets(conf, true)
if not conf then
return HTTP_INTERNAL_SERVER_ERROR, "failed to retrieve secrets from
conf"
end
diff --git a/apisix/plugins/authz-keycloak.lua
b/apisix/plugins/authz-keycloak.lua
index 34a053332..12b2fad5a 100644
--- a/apisix/plugins/authz-keycloak.lua
+++ b/apisix/plugins/authz-keycloak.lua
@@ -764,7 +764,7 @@ end
function _M.access(conf, ctx)
-- resolve secrets
- conf = fetch_secrets(conf, true, conf, "")
+ conf = fetch_secrets(conf, true)
local headers = core.request.headers(ctx)
local need_grant_token = conf.password_grant_token_generation_incoming_uri
and
ctx.var.request_uri ==
conf.password_grant_token_generation_incoming_uri and
diff --git a/apisix/plugins/limit-count.lua b/apisix/plugins/limit-count.lua
index 735779234..18beb5b78 100644
--- a/apisix/plugins/limit-count.lua
+++ b/apisix/plugins/limit-count.lua
@@ -34,7 +34,7 @@ end
function _M.access(conf, ctx)
- conf = fetch_secrets(conf, true, conf, "")
+ conf = fetch_secrets(conf, true)
return limit_count.rate_limit(conf, ctx, plugin_name, 1)
end
diff --git a/apisix/plugins/openid-connect.lua
b/apisix/plugins/openid-connect.lua
index 5afac47fe..3682e1bc0 100644
--- a/apisix/plugins/openid-connect.lua
+++ b/apisix/plugins/openid-connect.lua
@@ -550,7 +550,7 @@ end
function _M.rewrite(plugin_conf, ctx)
local conf_clone = core.table.clone(plugin_conf)
- local conf = fetch_secrets(conf_clone, true, plugin_conf, "")
+ local conf = fetch_secrets(conf_clone, true)
-- Previously, we multiply conf.timeout before storing it in etcd.
-- If the timeout is too large, we should not multiply it again.
diff --git a/apisix/secret.lua b/apisix/secret.lua
index b8d7b19a5..8ad1be260 100644
--- a/apisix/secret.lua
+++ b/apisix/secret.lua
@@ -51,7 +51,7 @@ local function check_secret(conf)
end
- local function secret_kv(manager, confid)
+local function secret_kv(manager, confid)
local secret_values
secret_values = core.config.fetch_created_obj("/secrets")
if not secret_values or not secret_values.values then
@@ -136,7 +136,7 @@ local function parse_secret_uri(secret_uri)
end
-local function fetch_by_uri(secret_uri)
+local function fetch_by_uri_secret(secret_uri)
core.log.info("fetching data from secret uri: ", secret_uri)
local opts, err = parse_secret_uri(secret_uri)
if not opts then
@@ -162,29 +162,7 @@ local function fetch_by_uri(secret_uri)
end
-- for test
-_M.fetch_by_uri = fetch_by_uri
-
-
-local function fetch(uri)
- -- do a quick filter to improve retrieval speed
- if byte(uri, 1, 1) ~= byte('$') then
- return nil
- end
-
- local val, err
- if string.has_prefix(upper(uri), core.env.PREFIX) then
- val, err = core.env.fetch_by_uri(uri)
- elseif string.has_prefix(uri, PREFIX) then
- val, err = fetch_by_uri(uri)
- end
-
- if err then
- core.log.error("failed to fetch secret value: ", err)
- return
- end
-
- return val
-end
+_M.fetch_by_uri = fetch_by_uri_secret
local function new_lrucache()
@@ -192,51 +170,85 @@ local function new_lrucache()
if not ttl then
ttl = 300
end
+
local count = core.table.try_read_attr(local_conf, "apisix", "lru",
"secret", "count")
if not count then
count = 512
end
- core.log.info("secret lrucache ttl: ", ttl, ", count: ", count)
+
+ local neg_ttl = core.table.try_read_attr(local_conf, "apisix", "lru",
"secret", "neg_ttl")
+ if not neg_ttl then
+ neg_ttl = 60 -- 1 minute default for failures
+ end
+
+ local neg_count = core.table.try_read_attr(local_conf, "apisix", "lru",
"secret", "neg_count")
+ if not neg_count then
+ neg_count = 512
+ end
+
+ core.log.info("secret lrucache ttl: ", ttl, ", count: ", count,
+ ", neg_ttl: ", neg_ttl, ", neg_count: ", neg_count)
+
return core.lrucache.new({
- ttl = ttl, count = count, invalid_stale = true, refresh_stale = true
+ ttl = ttl,
+ count = count,
+ neg_ttl = neg_ttl,
+ neg_count = neg_count,
+ invalid_stale = true,
+ refresh_stale = true
})
end
-local secrets_lrucache = new_lrucache()
-
-
-local fetch_secrets
-do
- local retrieve_refs
- function retrieve_refs(refs)
- for k, v in pairs(refs) do
- local typ = type(v)
- if typ == "string" then
- refs[k] = fetch(v) or v
- elseif typ == "table" then
- retrieve_refs(v)
- end
- end
- return refs
- end
- local function retrieve(refs)
- core.log.info("retrieve secrets refs")
+local secrets_cache = new_lrucache()
- local new_refs = core.table.deepcopy(refs)
- return retrieve_refs(new_refs)
+
+
+local function fetch(uri, use_cache)
+ -- do a quick filter to improve retrieval speed
+ if byte(uri, 1, 1) ~= byte('$') then
+ return nil
end
- function fetch_secrets(refs, cache, key, version)
- if not refs or type(refs) ~= "table" then
+ local fetch_by_uri
+ if string.has_prefix(upper(uri), core.env.PREFIX) then
+ fetch_by_uri = core.env.fetch_by_uri
+ elseif string.has_prefix(uri, PREFIX) then
+ fetch_by_uri = fetch_by_uri_secret
+ else
+ return nil
+ end
+
+ if not use_cache then
+ local val, err = fetch_by_uri(uri)
+ if err then
+ core.log.error("failed to fetch secret value: ", err)
return nil
end
- if not cache then
- return retrieve(refs)
+ return val
+ end
+
+ return secrets_cache(uri, "", fetch_by_uri, uri)
+end
+
+local function retrieve_refs(refs, use_cache)
+ for k, v in pairs(refs) do
+ local typ = type(v)
+ if typ == "string" then
+ refs[k] = fetch(v, use_cache) or v
+ elseif typ == "table" then
+ retrieve_refs(v, use_cache)
end
- return secrets_lrucache(key, version, retrieve, refs)
end
+ return refs
end
-_M.fetch_secrets = fetch_secrets
+function _M.fetch_secrets(refs, use_cache)
+ if not refs or type(refs) ~= "table" then
+ return nil
+ end
+
+ local new_refs = core.table.deepcopy(refs)
+ return retrieve_refs(new_refs, use_cache)
+end
return _M
diff --git a/apisix/ssl/router/radixtree_sni.lua
b/apisix/ssl/router/radixtree_sni.lua
index f12be1298..6104dcb10 100644
--- a/apisix/ssl/router/radixtree_sni.lua
+++ b/apisix/ssl/router/radixtree_sni.lua
@@ -241,7 +241,7 @@ function _M.set(matched_ssl, sni)
end
ngx_ssl.clear_certs()
- local new_ssl_value = secret.fetch_secrets(matched_ssl.value, true,
matched_ssl.value, "")
+ local new_ssl_value = secret.fetch_secrets(matched_ssl.value, true)
or matched_ssl.value
ok, err = _M.set_cert_and_key(sni, new_ssl_value)
diff --git a/conf/config.yaml.example b/conf/config.yaml.example
index d06d689aa..139e30edc 100644
--- a/conf/config.yaml.example
+++ b/conf/config.yaml.example
@@ -148,8 +148,10 @@ apisix:
# fine tune the parameters of LRU cache for some features like secret
lru:
secret:
- ttl: 300 # seconds
- count: 512
+ ttl: 300 # Global TTL fallback
+ count: 512 # Cache size
+ neg_ttl: 60 # Negative cache TTL
+ neg_count: 512 # Negative cache size
nginx_config: # Config for render the template to generate
nginx.conf
# user: root # Set the execution user of the worker
process. This is only
# effective if the master process runs with
super-user privileges.
diff --git a/t/config-center-json/secret.t b/t/config-center-json/secret.t
index 178f19ef5..ae3cc664b 100644
--- a/t/config-center-json/secret.t
+++ b/t/config-center-json/secret.t
@@ -385,7 +385,7 @@ qr/retrieve secrets refs/
-=== TEST 14: fetch_secrets env: cache
+=== TEST 14: fetch_secrets env: cache (fetch data should be only called once
and next call return from cache)
--- main_config
env secret=apisix;
--- config
@@ -396,9 +396,8 @@ env secret=apisix;
key = "jack",
secret = "$env://secret"
}
- local refs_1 = secret.fetch_secrets(refs, true, "key", 1)
- local refs_2 = secret.fetch_secrets(refs, true, "key", 1)
- assert(refs_1 == refs_2)
+ local refs_1 = secret.fetch_secrets(refs, true)
+ local refs_2 = secret.fetch_secrets(refs, true)
ngx.say(refs_1.secret)
ngx.say(refs_2.secret)
}
@@ -409,9 +408,9 @@ GET /t
apisix
apisix
--- grep_error_log eval
-qr/retrieve secrets refs/
+qr/fetching data from env uri/
--- grep_error_log_out
-retrieve secrets refs
+fetching data from env uri
diff --git a/t/config-center-yaml/secret.t b/t/config-center-yaml/secret.t
index 82fefd3a5..569b9b143 100644
--- a/t/config-center-yaml/secret.t
+++ b/t/config-center-yaml/secret.t
@@ -328,9 +328,8 @@ env secret=apisix;
key = "jack",
secret = "$env://secret"
}
- local refs_1 = secret.fetch_secrets(refs, true, "key", 1)
- local refs_2 = secret.fetch_secrets(refs, true, "key", 1)
- assert(refs_1 == refs_2)
+ local refs_1 = secret.fetch_secrets(refs, true)
+ local refs_2 = secret.fetch_secrets(refs, true)
ngx.say(refs_1.secret)
ngx.say(refs_2.secret)
}
@@ -341,9 +340,9 @@ GET /t
apisix
apisix
--- grep_error_log eval
-qr/retrieve secrets refs/
+qr/fetching data from env uri/
--- grep_error_log_out
-retrieve secrets refs
+fetching data from env uri
diff --git a/t/core/lrucache2.t b/t/core/lrucache2.t
new file mode 100644
index 000000000..1da3e4b27
--- /dev/null
+++ b/t/core/lrucache2.t
@@ -0,0 +1,272 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+log_level("info");
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: negative cache basic functionality
+--- config
+ location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+
+ local call_count = 0
+ local function create_obj_fail()
+ call_count = call_count + 1
+ return nil, "simulated failure"
+ end
+
+ -- create LRU cache with negative caching
+ local lru_get = core.lrucache.new({
+ ttl = 1,
+ count = 256,
+ neg_ttl = 0.5, -- shorter TTL for failures
+ neg_count = 128
+ })
+
+ -- First call should execute the function and cache the failure
+ local obj, err = lru_get("fail_key", "v1", create_obj_fail)
+ ngx.say("call_count after first call: ", call_count)
+ ngx.say("first call result: obj=", tostring(obj), ", err=",
tostring(err))
+
+ -- Second call should return from negative cache without calling
create_obj_fail
+ obj, err = lru_get("fail_key", "v1", create_obj_fail)
+ ngx.say("call_count after second call: ", call_count)
+ ngx.say("second call result: obj=", tostring(obj), ", err=",
tostring(err))
+
+ -- Different version should bypass negative cache
+ obj, err = lru_get("fail_key", "v2", create_obj_fail)
+ ngx.say("call_count after different version: ", call_count)
+ ngx.say("different version result: obj=", tostring(obj), ", err=",
tostring(err))
+ }
+ }
+--- request
+GET /t
+--- response_body
+call_count after first call: 1
+first call result: obj=nil, err=simulated failure
+call_count after second call: 1
+second call result: obj=nil, err=simulated failure
+call_count after different version: 2
+different version result: obj=nil, err=simulated failure
+
+
+
+=== TEST 2: negative cache TTL expiration
+--- config
+ location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+
+ local call_count = 0
+ local function create_obj_fail()
+ call_count = call_count + 1
+ return nil, "simulated failure"
+ end
+
+ -- Create LRU cache with very short negative TTL
+ local lru_get = core.lrucache.new({
+ ttl = 10,
+ count = 256,
+ neg_ttl = 0.1, -- very short TTL for failures
+ neg_count = 128
+ })
+
+ -- First call
+ local obj, err = lru_get("fail_key", "v1", create_obj_fail)
+ ngx.say("call_count after first call: ", call_count)
+
+ -- Immediate second call - should use negative cache
+ obj, err = lru_get("fail_key", "v1", create_obj_fail)
+ ngx.say("call_count after immediate call: ", call_count)
+
+ -- Wait for negative cache to expire
+ ngx.sleep(0.15)
+
+ -- This should call create_obj_fail again
+ obj, err = lru_get("fail_key", "v1", create_obj_fail)
+ ngx.say("call_count after TTL expiration: ", call_count)
+ }
+ }
+--- request
+GET /t
+--- response_body
+call_count after first call: 1
+call_count after immediate call: 1
+call_count after TTL expiration: 2
+
+
+
+=== TEST 3: mixed success and failure caching
+--- config
+ location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+
+ local success_count = 0
+ local fail_count = 0
+
+ local function create_obj_success()
+ success_count = success_count + 1
+ return {value = "success_" .. success_count}
+ end
+
+ local function create_obj_fail()
+ fail_count = fail_count + 1
+ return nil, "failure_" .. fail_count
+ end
+
+ local lru_get = core.lrucache.new({
+ ttl = 1,
+ count = 256,
+ neg_ttl = 0.5,
+ neg_count = 128
+ })
+
+ -- Test success caching
+ local obj1 = lru_get("success_key", "v1", create_obj_success)
+ ngx.say("success_count after first success: ", success_count)
+ ngx.say("success value: ", obj1.value)
+
+ local obj2 = lru_get("success_key", "v1", create_obj_success)
+ ngx.say("success_count after cached success: ", success_count)
+ ngx.say("cached success value: ", obj2.value)
+
+ -- Test failure caching
+ local obj3, err3 = lru_get("fail_key", "v1", create_obj_fail)
+ ngx.say("fail_count after first failure: ", fail_count)
+ ngx.say("failure error: ", err3)
+
+ local obj4, err4 = lru_get("fail_key", "v1", create_obj_fail)
+ ngx.say("fail_count after cached failure: ", fail_count)
+ ngx.say("cached failure error: ", err4)
+ }
+ }
+--- request
+GET /t
+--- response_body
+success_count after first success: 1
+success value: success_1
+success_count after cached success: 1
+cached success value: success_1
+fail_count after first failure: 1
+failure error: failure_1
+fail_count after cached failure: 1
+cached failure error: failure_1
+
+
+
+=== TEST 4: negative cache with different keys
+--- config
+ location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+
+ local call_count = 0
+ local function create_obj_fail(key)
+ call_count = call_count + 1
+ return nil, "failed for " .. key
+ end
+
+ local lru_get = core.lrucache.new({
+ ttl = 1,
+ count = 256,
+ neg_ttl = 0.5,
+ neg_count = 128
+ })
+
+ -- First key
+ local obj1, err1 = lru_get("key1", "v1", create_obj_fail, "key1")
+ ngx.say("call_count after key1: ", call_count)
+
+ -- Second key
+ local obj2, err2 = lru_get("key2", "v1", create_obj_fail, "key2")
+ ngx.say("call_count after key2: ", call_count)
+
+ -- Repeat key1 - should use negative cache
+ local obj3, err3 = lru_get("key1", "v1", create_obj_fail, "key1")
+ ngx.say("call_count after key1 repeat: ", call_count)
+ ngx.say("key1 error: ", err3)
+
+ -- Repeat key2 - should use negative cache
+ local obj4, err4 = lru_get("key2", "v1", create_obj_fail, "key2")
+ ngx.say("call_count after key2 repeat: ", call_count)
+ ngx.say("key2 error: ", err4)
+ }
+ }
+--- request
+GET /t
+--- response_body
+call_count after key1: 1
+call_count after key2: 2
+call_count after key1 repeat: 2
+key1 error: failed for key1
+call_count after key2 repeat: 2
+key2 error: failed for key2
+
+
+
+=== TEST 5: negative cache respects version changes
+--- config
+ location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+
+ local call_count = 0
+ local function create_obj_fail(version)
+ call_count = call_count + 1
+ return nil, "failed for version " .. version
+ end
+
+ local lru_get = core.lrucache.new({
+ ttl = 10,
+ count = 256,
+ neg_ttl = 10,
+ neg_count = 128
+ })
+
+ -- Call with version 1
+ local obj1, err1 = lru_get("version_key", "v1", create_obj_fail,
"v1")
+ ngx.say("call_count after v1: ", call_count)
+
+ -- Call with version 1 again - should use negative cache
+ local obj2, err2 = lru_get("version_key", "v1", create_obj_fail,
"v1")
+ ngx.say("call_count after v1 repeat: ", call_count)
+
+ -- Call with version 2 - should bypass negative cache
+ local obj3, err3 = lru_get("version_key", "v2", create_obj_fail,
"v2")
+ ngx.say("call_count after v2: ", call_count)
+
+ -- Call with version 2 again - should use negative cache
+ local obj4, err4 = lru_get("version_key", "v2", create_obj_fail,
"v2")
+ ngx.say("call_count after v2 repeat: ", call_count)
+ }
+ }
+--- request
+GET /t
+--- response_body
+call_count after v1: 1
+call_count after v1 repeat: 1
+call_count after v2: 2
+call_count after v2 repeat: 2