diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua index 35212eea7cf1..1dd9364f476b 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 374d33c908e5..6999685ab368 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 d229b47b25fe..2cf45d6d25e7 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 34a0533326f9..12b2fad5a5e2 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 735779234b30..18beb5b78e3f 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 5afac47fefe4..3682e1bc07e3 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 b8d7b19a522c..8ad1be260012 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 f12be1298680..6104dcb10bc9 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 d06d689aa110..139e30edc367 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 178f19ef5b09..ae3cc664bb01 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 82fefd3a576b..569b9b143afb 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 000000000000..1da3e4b2799d --- /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