After upgrading to Kong 3.7, the configuration of the plugin is inconsistent with the configuration in the database

After upgrading to Kong 3.7, it was observed that the custom plugin’s configuration became inconsistent with the configuration stored in the database during testing. The identical test case operates correctly when run on Kong version 3.2.2.

The custom plugin, named circuit-breaker, utilizes the lua-circuit-breaker library.

The main code of the plugin is as follows.

schema:

local typedefs = require "kong.db.schema.typedefs"

return {
	name = "circuit-breaker",
	fields = {
		{
			consumer = typedefs.no_consumer
		},
		{
			protocols = typedefs.protocols_http
		},
		{
			config = {
				type = "record",
				fields = {
					{window_size_seconds = {type = "number", gt = 0, required = true, default = 10}},
					{min_requests_in_window = {type = "number", gt = 1, required = true, default = 20}},
					{failure_percent_threshold = {type = "number", gt = 0, required = true, default = 51}},
					{open_state_wait_seconds = {type = "number", gt = 0, required = true, default = 15}},
					{half_open_wait_seconds = {type = "number", gt = 0, required = true, default = 120}},
					{half_open_min_requests = {type = "number", gt = 1, required = true, default = 5}},
					{half_open_max_requests = {type = "number", gt = 1, required = true, default = 10}},
					{open_state_status_code = {type = "number", required = true, default = 599}},
					{open_state_error_msg = {type = "string"}},
					{shadow_mode_enabled = {type = "boolean", required = false, default = true}},
				},
			}
		}
	}
}
function CircuitBreakerHandler:access(conf)
	local success, err = pcall(p_access, conf)
	if not success then
		kong.log.err("Error in cb access phase " .. err)
		return
	end
end
local function p_access(conf)
	conf.version = DEFAULT_VERSION
	local cb = get_circuit_breaker(conf)

	-- Check state of cb. This function returns an error if the state is open or half_open_max_calls_in_window is breached.
	local _, err_cb = cb:_before()
	if err_cb then
		if not conf.shadow_mode_enabled then
			local headers = {["My-Monitoring"] = conf.open_state_status_code} -- To observe open_state_status_code in the test
			return kong.response.exit(conf.open_state_status_code, conf.open_state_error_msg or err_cb, headers)
		end
	end

	kong.ctx.plugin.cb = cb
	kong.ctx.plugin.generation = cb._generation -- generation is used to keep track the counter in the correct time bucket.
end

As inferred from the preceding code, when the circuit breaker is opened, the status code for the response should be determined by open_state_status_code.

Tests on the plugin uses the same integration testing approach as for the official bundled plugins of the Kong project. Tests for LUA code are done using Busted unit testing framework.

The testing environment is managed by helpers and fixtures copied from the Kong project
This test is run in a container based on the kong image.

The main code of the test is as follows.

local helpers = require "spec.helpers"

local mock_service = require "util.mock_service"

local TEST_HOST_NAME = "host.test.com"
local TEST_PATH = "/echo"
local TEST_METHOD = "GET"

local function print_table (tbl, indent)
  if not indent then indent = 0 end
  if type(tbl) == "table" then
      for k, v in pairs(tbl) do
        formatting = string.rep("  ", indent) .. k .. ": "
        if type(v) == "table" then
          print(formatting)
          print_table(v, indent+1)
        elseif type(v) == 'boolean' then
          print(formatting .. tostring(v))
        elseif type(v) == 'function' then
          print(formatting .. tostring("function"))
        else
          print(formatting .. v)
        end
      end
  end
end

for _, strategy in helpers.each_strategy() do
    describe("circuit breaker plugin", function()
        local window_size_seconds = 10
        local open_state_wait_seconds = 3
        local half_open_min_requests = 2
        local half_open_max_requests = 4
        local min_requests_in_window = 6
        local failure_percent_threshold = 50
        local half_open_wait_seconds = 15
        local cb_error_status_code = 599
        local shadow_mode_enabled = false

        local bp, db = helpers.get_db_utils(strategy, {"routes", "services", "plugins"}, {"circuit-breaker"});

        local service = assert(bp.services:insert({
                protocol = "http",
                host = mock_service.MOCK_SERVICE_HOST,
                port = mock_service.MOCK_SERVICE_PORT,
                name = "test",
                connect_timeout = 5
            }))

        assert(bp.routes:insert({
            methods = {TEST_METHOD},
            protocols = {"http"},
            hosts = {TEST_HOST_NAME},
            paths = {TEST_PATH},
            strip_path = false,
            preserve_host = true,
            service = service,
        }))

        local circuit_breaker_plugin = bp.plugins:insert{
            id = "90f0321d-fd30-4e71-b341-f7eea48eedf7",
            name = "circuit-breaker",
            service = service,
            config = {
                min_requests_in_window = min_requests_in_window,
                window_size_seconds = window_size_seconds,
                failure_percent_threshold = failure_percent_threshold,
                open_state_wait_seconds = open_state_wait_seconds,
                half_open_wait_seconds = half_open_wait_seconds,
                half_open_max_requests = half_open_max_requests,
                half_open_min_requests = half_open_min_requests,
                open_state_status_code = cb_error_status_code,
                shadow_mode_enabled = shadow_mode_enabled
            }
        }

        local change_config = function (patch)
            local admin_client = helpers.admin_client()
            local url = "/plugins/" .. circuit_breaker_plugin["id"]

            local admin_res = assert(
                                    admin_client:patch(url, {
                    headers = {["Content-Type"] = "application/json"},
                    body = {
                        name = "circuit-breaker",
                        config = patch
                    }
                }))
            assert.res_status(200, admin_res)
            admin_client:close()
        end

        local get_cb_config = function (status_expected)
            local proxy_client = helpers.proxy_client()
            local res = assert(proxy_client:get("/echo/default",
                {
                    headers = {
                        response_http_code = res_status_to_be_generated,
                        response_latency_seconds = latency or 0,
                        ["Host"] = TEST_HOST_NAME,
                    },
                }))
            print("\nExpected status: " .. status_expected)
            print("Actual status: " .. res.status)
            print("\n====== detailed res: ======")
            print_table(res, 1)
            print("  res.body: " .. res:read_body())
            proxy_client:close()
            print("\n====== config from database: ======")
            local plugin, err = db.plugins:select({ id = "90f0321d-fd30-4e71-b341-f7eea48eedf7" })
            if err then
                print(err)
            else
                print_table(plugin.config, 1)
            end
        end

        before_each(function ()
            assert(helpers.start_kong({
                database = strategy,
                plugins = "circuit-breaker"
            }))
        end)
        after_each(function ()
            helpers.stop_kong()
        end)

        it("should create new circuit breaker on config change", function()
            local new_cb_error_status_code = 598
            change_config({open_state_status_code = new_cb_error_status_code})
            get_cb_config(new_cb_error_status_code)
        end)
    end)
end

The change_config function is designed to modify the open_state_status_code from its default value of 599 to 598. Therefore, the anticipated status code for this response should be 598. However, when the status code is retrieved through the helpers.proxy_client, it remains at 599. Interestingly, when the value is directly fetched from the database, it correctly shows as 598. This indicates that the change_config function has indeed successfully altered the open_state_status_code.

The same test code operates without issues on Kong version 3.2.2. However, upon upgrading to Kong version 3.7.0 (which includes updates to the Kong image, helpers, fixtures, and busted), an inconsistency arises between the response status and the value stored in the database.

Below is a screenshot of the test.

I would appreciate any insights or suggestions to resolve this issue. Thank you in advance!

I think the problem may be due to the configuration taking some time to propagate to all workers. You update the plugin config and then immediately proxy traffic; could you add a call to the helpers.wait_for_all_config_update() function in between change_config and get_cb_config ?

        it("should create new circuit breaker on config change", function()
            local new_cb_error_status_code = 598
            change_config({open_state_status_code = new_cb_error_status_code})
            helpers.wait_for_all_config_update()
            get_cb_config(new_cb_error_status_code)
        end)
1 Like

Your suggestion has successfully resolved the issue at hand. :+1:
I am deeply grateful for your assistance. Thank you very much.

1 Like

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.