项目文件夹

文件
2026-07-13 12:32:21 +08:00

445 行
12 KiB
Lua

local _M = {}
local _MT = { __index = _M, }
local semaphore = require("ngx.semaphore")
local config_helper = require("kong.clustering.config_helper")
local clustering_utils = require("kong.clustering.utils")
local declarative = require("kong.db.declarative")
local constants = require("kong.constants")
local inspect = require("inspect")
local assert = assert
local setmetatable = setmetatable
local math = math
local tostring = tostring
local sub = string.sub
local ngx = ngx
local ngx_log = ngx.log
local ngx_sleep = ngx.sleep
local json_decode = clustering_utils.json_decode
local json_encode = clustering_utils.json_encode
local exiting = ngx.worker.exiting
local ngx_time = ngx.time
local inflate_gzip = require("kong.tools.gzip").inflate_gzip
local yield = require("kong.tools.yield").yield
local ngx_ERR = ngx.ERR
local ngx_INFO = ngx.INFO
local ngx_DEBUG = ngx.DEBUG
local ngx_WARN = ngx.WARN
local ngx_NOTICE = ngx.NOTICE
local PING_INTERVAL = constants.CLUSTERING_PING_INTERVAL
local PING_WAIT = PING_INTERVAL * 1.5
local _log_prefix = "[clustering] "
local DECLARATIVE_EMPTY_CONFIG_HASH = constants.DECLARATIVE_EMPTY_CONFIG_HASH
local prev_hash
local endswith = require("pl.stringx").endswith
local function is_timeout(err)
return err and sub(err, -7) == "timeout"
end
function _M.new(clustering)
assert(type(clustering) == "table",
"kong.clustering is not instantiated")
assert(type(clustering.conf) == "table",
"kong.clustering did not provide configuration")
assert(type(clustering.cert) == "table",
"kong.clustering did not provide the cluster certificate")
assert(type(clustering.cert_key) == "cdata",
"kong.clustering did not provide the cluster certificate private key")
assert(kong.db.declarative_config,
"kong.db.declarative_config was not initialized")
local self = {
declarative_config = kong.db.declarative_config,
conf = clustering.conf,
cert = clustering.cert,
cert_key = clustering.cert_key,
-- in konnect_mode, reconfigure errors will be reported to the control plane
-- via WebSocket message
error_reporting = clustering.conf.konnect_mode,
}
return setmetatable(self, _MT)
end
local function set_control_plane_connected(reachable)
local ok, err = ngx.shared.kong:safe_set("control_plane_connected", reachable, PING_WAIT)
if not ok then
ngx_log(ngx_ERR, _log_prefix, "failed to set control_plane_connected key in shm to ", reachable, " :", err)
end
end
function _M:init_worker(basic_info)
-- ROLE = "data_plane"
self.plugins_list = basic_info.plugins
self.filters = basic_info.filters
set_control_plane_connected(false)
local function start_communicate()
assert(ngx.timer.at(0, function(premature)
self:communicate(premature)
end))
end
-- does not config rpc sync
if not kong.sync then
-- start communicate()
self.run_communicate = true
start_communicate()
return
end
local worker_events = assert(kong.worker_events)
-- if rpc is ready we will check then decide how to sync
worker_events.register(function(capabilities_list)
local has_sync_v2
-- check cp's capabilities
for _, v in ipairs(capabilities_list) do
if v == "kong.sync.v2" then
has_sync_v2 = true
break
end
end
-- cp supports kong.sync.v2
if has_sync_v2 then
-- notify communicate() to exit
self.run_communicate = false
return
end
-- start communicate()
self.run_communicate = true
ngx_log(ngx_WARN, "sync v1 is enabled due to rpc sync can not work.")
-- only run in process which worker_id() == 0
start_communicate()
end, "clustering:jsonrpc", "connected")
end
local function send_ping(c, log_suffix)
log_suffix = log_suffix or ""
local hash = declarative.get_current_hash()
if hash == "" or type(hash) ~= "string" then
hash = DECLARATIVE_EMPTY_CONFIG_HASH
end
local _, err = c:send_ping(hash)
if err then
set_control_plane_connected(false)
ngx_log(is_timeout(err) and ngx_NOTICE or ngx_WARN, _log_prefix,
"unable to send ping frame to control plane: ", err, log_suffix)
else
set_control_plane_connected(true)
-- only log a ping if the hash changed
if hash ~= prev_hash then
prev_hash = hash
ngx_log(ngx_INFO, _log_prefix, "sent ping frame to control plane with hash: ", hash, log_suffix)
end
end
end
---@param c resty.websocket.client
---@param err_t kong.clustering.config_helper.update.err_t
---@param log_suffix? string
local function send_error(c, err_t, log_suffix)
local payload, json_err = json_encode({
type = "error",
error = err_t,
})
if json_err then
json_err = tostring(json_err)
ngx_log(ngx_ERR, _log_prefix, "failed to JSON-encode error payload for ",
"control plane: ", json_err, ", payload: ", inspect(err_t), log_suffix)
payload = assert(json_encode({
type = "error",
error = {
name = constants.CLUSTERING_DATA_PLANE_ERROR.GENERIC,
message = "failed to encode JSON error payload: " .. json_err,
source = "kong.clustering.data_plane.send_error",
config_hash = err_t and err_t.config_hash
or DECLARATIVE_EMPTY_CONFIG_HASH,
}
}))
end
local ok, err = c:send_binary(payload)
if not ok then
ngx_log(ngx_ERR, _log_prefix, "failed to send error report to control plane: ",
err, log_suffix)
end
end
function _M:communicate(premature)
if premature then
-- worker wants to exit
return
end
local conf = self.conf
local log_suffix = " [" .. conf.cluster_control_plane .. "]"
local reconnection_delay = math.random(5, 10)
local c, uri, err = clustering_utils.connect_cp(self, "/v1/outlet")
if not c then
set_control_plane_connected(false)
ngx_log(ngx_WARN, _log_prefix, "connection to control plane ", uri, " broken: ", err,
" (retrying after ", reconnection_delay, " seconds)", log_suffix)
assert(ngx.timer.at(reconnection_delay, function(premature)
self:communicate(premature)
end))
return
end
local labels do
if kong.configuration.cluster_dp_labels then
labels = {}
for _, lab in ipairs(kong.configuration.cluster_dp_labels) do
local del = lab:find(":", 1, true)
labels[lab:sub(1, del - 1)] = lab:sub(del + 1)
end
end
end
local configuration = kong.configuration.remove_sensitive()
-- connection established
-- first, send out the plugin list and DP labels to CP
-- The CP will make the decision on whether sync will be allowed
-- based on the received information
local _
_, err = c:send_binary(json_encode({ type = "basic_info",
plugins = self.plugins_list,
process_conf = configuration,
filters = self.filters,
labels = labels, }))
if err then
set_control_plane_connected(false)
ngx_log(ngx_ERR, _log_prefix, "unable to send basic information to control plane: ", uri,
" err: ", err, " (retrying after ", reconnection_delay, " seconds)", log_suffix)
c:close()
assert(ngx.timer.at(reconnection_delay, function(premature)
self:communicate(premature)
end))
return
end
set_control_plane_connected(true)
local config_semaphore = semaphore.new(0)
-- how DP connection management works:
-- three threads are spawned, when any of these threads exits,
-- it means a fatal error has occurred on the connection,
-- and the other threads are also killed
--
-- * config_thread: it grabs a received declarative config and apply it
-- locally. In addition, this thread also persists the
-- config onto the local file system
-- * read_thread: it is the only thread that sends WS frames to the CP
-- by sending out periodic PING frames to CP that checks
-- for the healthiness of the WS connection. In addition,
-- PING messages also contains the current config hash
-- applied on the local Kong DP
-- * write_thread: it is the only thread that receives WS frames from the CP,
-- and is also responsible for handling timeout detection
local ping_immediately
local config_exit
local next_data
local config_err_t
local config_thread = ngx.thread.spawn(function()
-- outside flag will stop the communicate() loop
while not exiting() and not config_exit and self.run_communicate do
local ok, err = config_semaphore:wait(1)
if not ok then
if err ~= "timeout" then
ngx_log(ngx_ERR, _log_prefix, "semaphore wait error: ", err)
end
goto continue
end
local data = next_data
if not data then
goto continue
end
local msg = assert(inflate_gzip(data))
yield()
msg = assert(json_decode(msg))
yield()
if msg.type ~= "reconfigure" then
goto continue
end
ngx_log(ngx_DEBUG, _log_prefix, "received reconfigure frame from control plane",
msg.timestamp and " with timestamp: " .. msg.timestamp or "",
log_suffix)
local err_t
ok, err, err_t = config_helper.update(self.declarative_config, msg)
if ok then
ping_immediately = true
else
if self.error_reporting then
config_err_t = err_t
end
ngx_log(ngx_ERR, _log_prefix, "unable to update running config: ", err)
end
if next_data == data then
next_data = nil
end
::continue::
end
end)
local write_thread = ngx.thread.spawn(function()
local counter = 0 -- count down to ping
while not exiting() do
if ping_immediately or counter <= 0 then
ping_immediately = nil
counter = PING_INTERVAL
send_ping(c, log_suffix)
end
if config_err_t then
local err_t = config_err_t
config_err_t = nil
send_error(c, err_t, log_suffix)
end
counter = counter - 1
ngx_sleep(1)
end
end)
local read_thread = ngx.thread.spawn(function()
local last_seen = ngx_time()
while not exiting() do
local data, typ, err = c:recv_frame()
if err then
if not is_timeout(err) then
set_control_plane_connected(false)
return nil, "error while receiving frame from control plane: " .. err
end
local waited = ngx_time() - last_seen
if waited > PING_WAIT then
set_control_plane_connected(false)
return nil, "did not receive pong frame from control plane within " .. PING_WAIT .. " seconds"
end
goto continue
end
set_control_plane_connected(true)
if typ == "close" then
ngx_log(ngx_DEBUG, _log_prefix, "received close frame from control plane", log_suffix)
return nil
end
last_seen = ngx_time()
if typ == "binary" then
next_data = data
if config_semaphore:count() <= 0 then
-- the following line always executes immediately after the `if` check
-- because `:count` will never yield, end result is that the semaphore
-- count is guaranteed to not exceed 1
config_semaphore:post()
end
goto continue
end
if typ == "pong" then
kong.log.trace(_log_prefix, "received pong frame from control plane", log_suffix)
goto continue
end
-- unknown websocket frame
ngx_log(ngx_NOTICE, _log_prefix,
"received unknown (", tostring(typ), ") frame from control plane",
log_suffix)
::continue::
end
end)
local ok, err, perr = ngx.thread.wait(read_thread, write_thread, config_thread)
ngx.thread.kill(read_thread)
ngx.thread.kill(write_thread)
c:close()
local err_msg = ok and err or perr
if err_msg and endswith(err_msg, ": closed") then
ngx_log(ngx_INFO, _log_prefix, "connection to control plane closed", log_suffix)
elseif err_msg then
ngx_log(ngx_ERR, _log_prefix, err_msg, log_suffix)
end
-- the config thread might be holding a lock if it's in the middle of an
-- update, so we need to give it a chance to terminate gracefully
config_exit = true
ok, err, perr = ngx.thread.wait(config_thread)
if not ok then
ngx_log(ngx_ERR, _log_prefix, err, log_suffix)
elseif perr then
ngx_log(ngx_ERR, _log_prefix, perr, log_suffix)
end
if not exiting() and self.run_communicate then
assert(ngx.timer.at(reconnection_delay, function(premature)
self:communicate(premature)
end))
end
end
return _M