This is an automated email from the ASF dual-hosted git repository.

ashishtiwari 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 2b774e5a5 fix(consumer): missed consumer update due to wrong version 
in cache (#12413)
2b774e5a5 is described below

commit 2b774e5a55c0a578c8717d74589b2d9f3429ae76
Author: Ashish Tiwari <ashishjaitiwari15112...@gmail.com>
AuthorDate: Fri Jul 11 10:33:04 2025 +0530

    fix(consumer): missed consumer update due to wrong version in cache (#12413)
---
 apisix/consumer.lua            |  33 ++++++---
 t/admin/consumer-credentials.t | 161 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 182 insertions(+), 12 deletions(-)

diff --git a/apisix/consumer.lua b/apisix/consumer.lua
index d69226b96..97b4c999f 100644
--- a/apisix/consumer.lua
+++ b/apisix/consumer.lua
@@ -94,7 +94,7 @@ do
             count = consumers_count_for_lrucache
         })
 
-local function construct_consumer_data(val, plugin_config)
+local function construct_consumer_data(val, name, plugin_config)
     -- if the val is a Consumer, clone it to the local consumer;
     -- if the val is a Credential, to get the Consumer by consumer_name and 
then clone
     -- it to the local consumer.
@@ -103,20 +103,28 @@ local function construct_consumer_data(val, plugin_config)
         local consumer_name = 
get_consumer_name_from_credential_etcd_key(val.key)
         local the_consumer = consumers:get(consumer_name)
         if the_consumer and the_consumer.value then
-            consumer = core.table.clone(the_consumer.value)
-            consumer.modifiedIndex = the_consumer.modifiedIndex
-            consumer.credential_id = get_credential_id_from_etcd_key(val.key)
+            consumer = consumers_id_lrucache(val.value.id .. name, 
val.modifiedIndex..
+                                                the_consumer.modifiedIndex,
+                function (val, the_consumer)
+                    consumer = core.table.clone(the_consumer.value)
+                    consumer.modifiedIndex = the_consumer.modifiedIndex
+                    consumer.credential_id = 
get_credential_id_from_etcd_key(val.key)
+                    return consumer
+                end, val, the_consumer)
         else
             -- Normally wouldn't get here:
             -- it should belong to a consumer for any credential.
-            core.log.error("failed to get the consumer for the credential,",
+            return nil, "failed to get the consumer for the credential,",
                 " a wild credential has appeared!",
-                " credential key: ", val.key, ", consumer name: ", 
consumer_name)
-            return nil, "failed to get the consumer for the credential"
+                " credential key: ", val.key, ", consumer name: ", 
consumer_name
         end
     else
-        consumer = core.table.clone(val.value)
-        consumer.modifiedIndex = val.modifiedIndex
+        consumer = consumers_id_lrucache(val.value.id .. name, 
val.modifiedIndex,
+            function (val)
+                consumer = core.table.clone(val.value)
+                consumer.modifiedIndex = val.modifiedIndex
+                return consumer
+            end, val)
     end
 
     -- if the consumer has labels, set the field custom_id to it.
@@ -160,9 +168,10 @@ function plugin_consumer()
                     }
                 end
 
-                local consumer = consumers_id_lrucache(val.value.id .. name,
-                        val.modifiedIndex, construct_consumer_data, val, 
config)
-                if consumer == nil then
+                local consumer, err = construct_consumer_data(val, name, 
config)
+                if not consumer then
+                    core.log.error("failed to construct consumer data for 
plugin ",
+                                   name, ": ", err)
                     goto CONTINUE
                 end
 
diff --git a/t/admin/consumer-credentials.t b/t/admin/consumer-credentials.t
new file mode 100644
index 000000000..060e253d7
--- /dev/null
+++ b/t/admin/consumer-credentials.t
@@ -0,0 +1,161 @@
+#
+# 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();
+no_shuffle();
+log_level("info");
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: Verify consumer plugin update takes effect immediately
+--- extra_yaml_config
+nginx_config:
+  worker_processes: 1
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local json = require("toolkit.json")
+            local http = require("resty.http")
+
+            -- 1. Create route with key-auth plugin
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/anything",
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "httpbin.org:80": 1
+                        }
+                    },
+                    "plugins": {
+                        "key-auth": {
+                            "query": "apikey"
+                        }
+                    }
+                }]]
+            )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say("Route creation failed: " .. body)
+                return
+            end
+
+            -- 2. Create consumer jack
+            code, body = t('/apisix/admin/consumers',
+                ngx.HTTP_PUT,
+                [[{
+                    "username": "jack"
+                }]]
+            )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say("Consumer creation failed: " .. body)
+                return
+            end
+
+            -- 3. Create credentials for jack
+            code, body = t('/apisix/admin/consumers/jack/credentials/auth-one',
+                ngx.HTTP_PUT,
+                [[{
+                    "plugins": {
+                        "key-auth": {
+                            "key": "auth-one"
+                        }
+                    }
+                }]]
+            )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say("Credential creation failed: " .. body)
+                return
+            end
+            ngx.sleep(0.5)  -- wait for etcd to sync
+            -- 4. Verify valid request succeeds
+            local httpc = http.new()
+            local res, err = httpc:request_uri(
+                
"http://127.0.0.1:"..ngx.var.server_port.."/anything?apikey=auth-one";,
+                { method = "GET" }
+            )
+            if not res then
+                ngx.say("Request failed: ", err)
+                return
+            end
+
+            if res.status ~= 200 then
+                ngx.say("Unexpected status: ", res.status)
+                ngx.say(res.body)
+                return
+            end
+
+            -- 5. Update consumer with fault-injection plugin
+            code, body = t('/apisix/admin/consumers/jack',
+                ngx.HTTP_PUT,
+                [[{
+                    "username": "jack",
+                    "plugins": {
+                        "fault-injection": {
+                            "abort": {
+                                "http_status": 400,
+                                "body": "abort"
+                            }
+                        }
+                    }
+                }]]
+            )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say("Consumer update failed: " .. body)
+                return
+            end
+
+            -- 6. Verify all requests return 400
+            for i = 1, 5 do
+                local res, err = httpc:request_uri(
+                    
"http://127.0.0.1:"..ngx.var.server_port.."/anything?apikey=auth-one";,
+                    { method = "GET" }
+                )
+                if not res then
+                    ngx.say(i, ": Request failed: ", err)
+                    return
+                end
+
+                if res.status ~= 400 then
+                    ngx.say(i, ": Expected 400 but got ", res.status)
+                    return
+                end
+
+                if res.body ~= "abort" then
+                    ngx.say(i, ": Unexpected response body: ", res.body)
+                    return
+                end
+            end
+
+            ngx.say("All requests aborted as expected")
+        }
+    }
+--- request
+GET /t
+--- response_body
+All requests aborted as expected

Reply via email to