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 04bb011a4 fix(secret): refresh stale lru cache item in background 
(#12614)
04bb011a4 is described below

commit 04bb011a4073370278324b6eb277d42d888d0279
Author: Nic <[email protected]>
AuthorDate: Tue Sep 16 20:36:53 2025 +0800

    fix(secret): refresh stale lru cache item in background (#12614)
    
    Signed-off-by: Nic <[email protected]>
---
 apisix/cli/config.lua     |  6 ++++
 apisix/core/lrucache.lua  | 83 ++++++++++++++++++++++++++++++++++++++---------
 apisix/init.lua           |  4 +++
 apisix/secret.lua         | 21 ++++++++++--
 conf/config.yaml.example  |  5 +++
 t/core/lrucache.t         | 40 ++---------------------
 t/router/radixtree-sni2.t | 64 +++++++++++++++++++++++++++++++++---
 7 files changed, 164 insertions(+), 59 deletions(-)

diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua
index 2c57541ba..0b9a23198 100644
--- a/apisix/cli/config.lua
+++ b/apisix/cli/config.lua
@@ -79,6 +79,12 @@ local _M = {
     },
     events = {
       module = "lua-resty-events"
+    },
+    lru = {
+      secret = {
+        ttl = 300,
+        count = 512
+      }
     }
   },
   nginx_config = {
diff --git a/apisix/core/lrucache.lua b/apisix/core/lrucache.lua
index 5c81dd317..374d33c90 100644
--- a/apisix/core/lrucache.lua
+++ b/apisix/core/lrucache.lua
@@ -22,9 +22,14 @@
 local lru_new = require("resty.lrucache").new
 local resty_lock = require("resty.lock")
 local log = require("apisix.core.log")
+local pairs = pairs
+local pcall = pcall
+local unpack = unpack
 local tostring = tostring
 local ngx = ngx
 local get_phase = ngx.get_phase
+local timer_every = ngx.timer.every
+local exiting = ngx.worker.exiting
 
 
 local lock_shdict_name = "lrucache-lock"
@@ -42,6 +47,8 @@ local can_yield_phases = {
     timer = true
 }
 
+local stale_obj_pool = {}
+
 local GLOBAL_ITEMS_COUNT = 1024
 local GLOBAL_TTL         = 60 * 60          -- 60 min
 local PLUGIN_TTL         = 5 * 60           -- 5 min
@@ -49,20 +56,28 @@ local PLUGIN_ITEMS_COUNT = 8
 local global_lru_fun
 
 
-local function fetch_valid_cache(lru_obj, invalid_stale, item_ttl,
-                                 item_release, key, version)
+local function fetch_valid_cache(lru_obj, invalid_stale, refresh_stale, 
item_ttl,
+                                 key, version, create_obj_fun, ...)
     local obj, stale_obj = lru_obj:get(key)
     if obj and obj.ver == version then
         return obj
     end
 
-    if not invalid_stale and stale_obj and stale_obj.ver == version then
-        lru_obj:set(key, stale_obj, item_ttl)
-        return stale_obj
-    end
+    if stale_obj and stale_obj.ver == version then
+        if not invalid_stale then
+            lru_obj:set(key, stale_obj, item_ttl)
+            return stale_obj
+        end
 
-    if item_release and obj then
-        item_release(obj.val)
+        if refresh_stale then
+            stale_obj_pool[lru_obj][key] = {
+                fn = create_obj_fun,
+                args = {...},
+                ver = version,
+                ttl = item_ttl,
+            }
+            return stale_obj
+        end
     end
 
     return nil
@@ -79,15 +94,16 @@ local function new_lru_fun(opts)
         item_ttl = opts and opts.ttl or GLOBAL_TTL
     end
 
-    local item_release = opts and opts.release
     local invalid_stale = opts and opts.invalid_stale
+    local refresh_stale = opts and opts.refresh_stale
     local serial_creating = opts and opts.serial_creating
     local lru_obj = lru_new(item_count)
+    stale_obj_pool[lru_obj] = {}
 
     return function (key, version, create_obj_fun, ...)
         if not serial_creating or not can_yield_phases[get_phase()] then
-            local cache_obj = fetch_valid_cache(lru_obj, invalid_stale,
-                                item_ttl, item_release, key, version)
+            local cache_obj = fetch_valid_cache(lru_obj, invalid_stale, 
refresh_stale,
+                                item_ttl, key, version, create_obj_fun, ...)
             if cache_obj then
                 return cache_obj.val
             end
@@ -100,8 +116,8 @@ local function new_lru_fun(opts)
             return obj, err
         end
 
-        local cache_obj = fetch_valid_cache(lru_obj, invalid_stale, item_ttl,
-                            item_release, key, version)
+        local cache_obj = fetch_valid_cache(lru_obj, invalid_stale, 
refresh_stale, item_ttl,
+                            key, version, create_obj_fun, ...)
         if cache_obj then
             return cache_obj.val
         end
@@ -119,8 +135,8 @@ local function new_lru_fun(opts)
             return nil, "failed to acquire the lock: " .. err
         end
 
-        cache_obj = fetch_valid_cache(lru_obj, invalid_stale, item_ttl,
-                        nil, key, version)
+        cache_obj = fetch_valid_cache(lru_obj, invalid_stale, refresh_stale, 
item_ttl,
+                        key, version, create_obj_fun, ...)
         if cache_obj then
             lock:unlock()
             log.info("unlock with key ", key_s)
@@ -181,8 +197,45 @@ local function plugin_ctx_id(api_ctx, extra_key)
 end
 
 
+local function refresh_stale_objs()
+    for lru_obj, keys in pairs(stale_obj_pool) do
+        for key, new_obj in pairs(keys) do
+            local obj, err = new_obj.fn(unpack(new_obj.args))
+            if obj ~= nil then
+                lru_obj:set(key, {val = obj, ver = new_obj.ver}, new_obj.ttl)
+                keys[key] = nil
+                log.info("successfully refresh stale obj for key ",
+                            tostring(key), " to ver ", new_obj.ver)
+            else
+                log.error("failed to refresh stale obj for key ", key, ": ", 
err)
+            end
+        end
+    end
+end
+
+
+local function init_worker()
+    local running = false
+    timer_every(1, function ()
+        if not exiting() then
+            if running then
+                log.info("timer_refresh_stale is already running, skipping 
this iteration")
+                return
+            end
+            running = true
+            local ok, err = pcall(refresh_stale_objs)
+            if not ok then
+                log.error("failed to run timer_refresh_stale: ", err)
+            end
+            running = false
+        end
+    end)
+end
+
+
 local _M = {
     version = 0.1,
+    init_worker = init_worker,
     new = new_lru_fun,
     global = global_lru_fun,
     plugin_ctx = plugin_ctx,
diff --git a/apisix/init.lua b/apisix/init.lua
index 5af93d142..d6f7a1a3d 100644
--- a/apisix/init.lua
+++ b/apisix/init.lua
@@ -117,6 +117,8 @@ function _M.http_init_worker()
 
     require("apisix.events").init_worker()
 
+    core.lrucache.init_worker()
+
     local discovery = require("apisix.discovery.init").discovery
     if discovery and discovery.init_worker then
         discovery.init_worker()
@@ -1088,6 +1090,8 @@ function _M.stream_init_worker()
     -- for testing only
     core.log.info("random stream test in [1, 10000]: ", math.random(1, 10000))
 
+    core.lrucache.init_worker()
+
     if core.config.init_worker then
         local ok, err = core.config.init_worker()
         if not ok then
diff --git a/apisix/secret.lua b/apisix/secret.lua
index 60e575b92..b8d7b19a5 100644
--- a/apisix/secret.lua
+++ b/apisix/secret.lua
@@ -19,6 +19,8 @@ local require   = require
 local core      = require("apisix.core")
 local string    = require("apisix.core.string")
 
+local local_conf = require("apisix.core.config_local").local_conf()
+
 local find      = string.find
 local sub       = string.sub
 local upper     = string.upper
@@ -185,9 +187,22 @@ local function fetch(uri)
 end
 
 
-local secrets_lrucache = core.lrucache.new({
-    ttl = 300, count = 512
-})
+local function new_lrucache()
+    local ttl = core.table.try_read_attr(local_conf, "apisix", "lru", 
"secret", "ttl")
+    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)
+    return core.lrucache.new({
+        ttl = ttl, count = count, invalid_stale = true, refresh_stale = true
+    })
+end
+local secrets_lrucache = new_lrucache()
+
 
 local fetch_secrets
 do
diff --git a/conf/config.yaml.example b/conf/config.yaml.example
index 408fb5138..7a0f6939c 100644
--- a/conf/config.yaml.example
+++ b/conf/config.yaml.example
@@ -141,6 +141,11 @@ apisix:
                                   # or (standalone mode) the config isn't 
loaded yet either via file or Admin API.
   # disable_upstream_healthcheck: false # A global switch for healthcheck. 
Defaults to false.
                                         # When set to true, it overrides all 
upstream healthcheck configurations and globally disabling healthchecks.
+  # fine tune the parameters of LRU cache for some features like secret
+  lru:
+    secret:
+      ttl: 300 # seconds
+      count: 512
 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/core/lrucache.t b/t/core/lrucache.t
index 0b75e8e61..905c20fd0 100644
--- a/t/core/lrucache.t
+++ b/t/core/lrucache.t
@@ -128,41 +128,7 @@ obj: 2
 
 
 
-=== TEST 4: sanity
---- config
-    location /t {
-        content_by_lua_block {
-            local core = require("apisix.core")
-
-            local function server_release(self)
-                ngx.say("release: ", require("toolkit.json").encode(self))
-            end
-
-            local lrucache_server_picker = core.lrucache.new({
-                ttl = 300, count = 256, release = server_release,
-            })
-
-            local t1 = lrucache_server_picker("nnn", "t1",
-                function () return {name = "aaa"} end)
-
-            ngx.say("obj: ", require("toolkit.json").encode(t1))
-
-            local t2 = lrucache_server_picker("nnn", "t2",
-                function () return {name = "bbb"} end)
-
-            ngx.say("obj: ", require("toolkit.json").encode(t2))
-        }
-    }
---- request
-GET /t
---- response_body
-obj: {"name":"aaa"}
-release: {"name":"aaa"}
-obj: {"name":"bbb"}
-
-
-
-=== TEST 5: invalid_stale = true
+=== TEST 4: invalid_stale = true
 --- config
     location /t {
         content_by_lua_block {
@@ -197,7 +163,7 @@ obj: {"idx":2}
 
 
 
-=== TEST 6: when creating cached objects, use resty-lock to avoid repeated 
creation.
+=== TEST 5: when creating cached objects, use resty-lock to avoid repeated 
creation.
 --- config
     location /t {
         content_by_lua_block {
@@ -233,7 +199,7 @@ obj: {"idx":1}
 
 
 
-=== TEST 7: different `key` and `ver`, cached same one table
+=== TEST 6: different `key` and `ver`, cached same one table
 --- config
     location /t {
         content_by_lua_block {
diff --git a/t/router/radixtree-sni2.t b/t/router/radixtree-sni2.t
index c761c9043..79d23df8e 100644
--- a/t/router/radixtree-sni2.t
+++ b/t/router/radixtree-sni2.t
@@ -697,7 +697,63 @@ ssl handshake: true
 
 
 
-=== TEST 18: set ssl conf with secret ref: env
+=== TEST 18: get cert and key from vault with a custom ttl
+--- yaml_config
+apisix:
+  lru:
+    secret:
+      ttl: 2
+      count: 1024
+--- config
+listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;
+location /t {
+    content_by_lua_block {
+        local tls_handshake = function()
+            local sock = ngx.socket.tcp()
+            sock:settimeout(2000)
+            local ok, err = 
sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock")
+            if not ok then
+                ngx.say("failed to connect: ", err)
+                return
+            end
+            local sess, err = sock:sslhandshake(nil, "test2.com", false)
+            if not sess then
+                ngx.say("failed to do SSL handshake: ", err)
+                return
+            end
+            sock:close()
+        end
+        -- send three requests within ttl
+        for i = 1, 3 do
+            tls_handshake()
+        end
+        -- wait for ttl to expire
+        ngx.sleep(2)
+        -- send one requests to trigger refresh
+        tls_handshake()
+        -- wait for refresh to complete
+        ngx.sleep(1)
+        -- send another three requests after refresh
+        for i = 1, 3 do
+            tls_handshake()
+        end
+        ngx.say("passed")
+    }
+}
+--- response_body
+passed
+--- error_log
+secret lrucache ttl: 2, count: 1024
+successfully refresh stale obj for key
+--- grep_error_log eval
+qr/fetching data from secret uri: 
\$secret:\/\/vault\/test1\/ssl\/test2.com.crt, context: \S+/
+--- grep_error_log_out
+fetching data from secret uri: $secret://vault/test1/ssl/test2.com.crt, 
context: ssl_certificate_by_lua*,
+fetching data from secret uri: $secret://vault/test1/ssl/test2.com.crt, 
context: ngx.timer
+
+
+
+=== TEST 19: set ssl conf with secret ref: env
 --- request
 GET /t
 --- config
@@ -726,7 +782,7 @@ passed
 
 
 
-=== TEST 19: get cert and key from env
+=== TEST 20: get cert and key from env
 --- config
 listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;
 
@@ -758,7 +814,7 @@ ssl handshake: true
 
 
 
-=== TEST 20: set ssl conf with secret ref: only cert use env
+=== TEST 21: set ssl conf with secret ref: only cert use env
 --- request
 GET /t
 --- config
@@ -791,7 +847,7 @@ passed
 
 
 
-=== TEST 21: get cert from env
+=== TEST 22: get cert from env
 --- config
 listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;
 

Reply via email to