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

Reply via email to