项目文件夹

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

406 行
11 KiB
Lua

local _M = {}
local _MT = { __index = _M, }
local uuid = require("resty.jit-uuid")
local queue = require("kong.clustering.rpc.queue")
local cjson = require("cjson")
local jsonrpc = require("kong.clustering.rpc.json_rpc_v2")
local rpc_utils = require("kong.clustering.rpc.utils")
local isarray = require("table.isarray")
local isempty = require("table.isempty")
local tb_insert = table.insert
local type = type
local setmetatable = setmetatable
local tostring = tostring
local pcall = pcall
local assert = assert
local string_format = string.format
local cjson_decode = cjson.decode
local cjson_encode = cjson.encode
local exiting = ngx.worker.exiting
local is_timeout = rpc_utils.is_timeout
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR
local ngx_WARN = ngx.WARN
local ngx_DEBUG = ngx.DEBUG
local new_error = jsonrpc.new_error
local RESP_CHANNEL_PREFIX = "rpc:resp:" -- format: rpc:resp:<worker_uuid>
local REQ_CHANNEL_PREFIX = "rpc:req:" -- format: rpc:req:<dst_node_id>
local RPC_REQUEST_ENQUEUE_SQL = [[
BEGIN;
INSERT INTO clustering_rpc_requests (
"node_id",
"reply_to",
"ttl",
"payload"
) VALUES (
%s,
%s,
CURRENT_TIMESTAMP(3) AT TIME ZONE 'UTC' + INTERVAL '%d second',
%s
);
SELECT pg_notify(%s, NULL);
COMMIT;
]]
local RPC_REQUEST_DEQUEUE_SQL = [[
BEGIN;
DELETE FROM
clustering_rpc_requests
USING (
SELECT * FROM clustering_rpc_requests WHERE node_id = %s FOR UPDATE SKIP LOCKED
) q
WHERE q.id = clustering_rpc_requests.id RETURNING clustering_rpc_requests.*;
COMMIT;
]]
function _M.new(manager, db)
local self = {
manager = manager,
db = db,
interest = {}, -- id: callback pair
sub_unsub = queue.new(4096), -- pub/sub event queue, executed on the read thread
sequence = 0,
}
return setmetatable(self, _MT)
end
function _M:_get_next_id()
local res = self.sequence
self.sequence = res + 1
return res
end
local function enqueue_notifications(notifications, notifications_queue)
assert(notifications_queue)
if notifications then
for _, n in ipairs(notifications) do
assert(notifications_queue:push(n))
end
end
end
function _M:process_one_response(payload)
assert(payload.jsonrpc == jsonrpc.VERSION)
local payload_id = payload.id
-- may be some error message for peer
if not payload_id then
if payload.error then
ngx_log(ngx_ERR, "[rpc] RPC failed, code: ",
payload.error.code, ", err: ",
payload.error.message)
end
return
end
-- response
local cb = self.interest[payload_id]
self.interest[payload_id] = nil -- edge trigger only once
if not cb then
ngx_log(ngx_WARN, "[rpc] no interest for concentrator response id: ", payload_id, ", dropping it")
return
end
local res, err = cb(payload)
if not res then
ngx_log(ngx_WARN, "[rpc] concentrator response interest handler failed: id: ",
payload_id, ", err: ", err)
end
end
function _M:process_one_request(target_id, reply_to, payload, collection)
if type(payload) ~= "table" then
local res, err = self:_enqueue_rpc_response(
reply_to,
new_error(nil, jsonrpc.INVALID_REQUEST, "not an valid object"),
collection)
if not res then
ngx_log(ngx_WARN, "[rpc] unable to enqueue RPC error: ", err)
end
return
end
local payload_id = payload.id
local res, err = self.manager:_local_call(target_id, payload.method,
payload.params, not payload_id)
-- notification has no callback or id
if not payload_id then
ngx_log(ngx_DEBUG, "[rpc] notification has no response")
return
end
if res then
-- call success
res, err = self:_enqueue_rpc_response(reply_to, {
jsonrpc = jsonrpc.VERSION,
id = payload_id,
result = res,
}, collection)
if not res then
ngx_log(ngx_WARN, "[rpc] unable to enqueue RPC call result: ", err)
end
else
-- call failure
res, err = self:_enqueue_rpc_response(reply_to, {
jsonrpc = jsonrpc.VERSION,
id = payload_id,
error = {
code = jsonrpc.SERVER_ERROR,
message = tostring(err),
}
}, collection)
if not res then
ngx_log(ngx_WARN, "[rpc] unable to enqueue RPC error: ", err)
end
end
end
function _M:_event_loop(lconn)
local notifications_queue = queue.new(4096)
local rpc_resp_channel_name = RESP_CHANNEL_PREFIX .. self.worker_id
-- we always subscribe to our worker's receiving channel first
local res, err = lconn:query('LISTEN "' .. rpc_resp_channel_name .. '";')
if not res then
return nil, "unable to subscribe to concentrator response channel: " .. err
end
while not exiting() do
while true do
local n, err = notifications_queue:pop(0)
if not n then
if err then
return nil, "unable to pop from notifications queue: " .. err
end
break
end
assert(n.operation == "notification")
if n.channel == rpc_resp_channel_name then
-- an response for a previous RPC call we asked for
local payload = cjson_decode(n.payload)
if not isarray(payload) then
-- one rpc response
self:process_one_response(payload)
else
-- batch rpc response
for _, v in ipairs(payload) do
self:process_one_response(v)
end
end
else
-- other CP inside the cluster asked us to forward a call
assert(n.channel:sub(1, #REQ_CHANNEL_PREFIX) == REQ_CHANNEL_PREFIX,
"unexpected concentrator request channel name: " .. n.channel)
local target_id = n.channel:sub(#REQ_CHANNEL_PREFIX + 1)
local sql = string_format(RPC_REQUEST_DEQUEUE_SQL, self.db.connector:escape_literal(target_id))
local calls, err = self.db.connector:query(sql)
if not calls then
return nil, "concentrator request dequeue query failed: " .. err
end
assert(calls[1] == true)
ngx_log(ngx_DEBUG, "concentrator got ", calls[2].affected_rows,
" calls from database for node ", target_id)
for _, call in ipairs(calls[2]) do
local payload = assert(call.payload)
local reply_to = assert(call.reply_to,
"unknown requester for RPC")
if not isarray(payload) then
-- one rpc call
self:process_one_request(target_id, reply_to, payload)
goto continue
end
-- rpc call with an empty Array
if isempty(payload) then
local res, err = self:_enqueue_rpc_response(
reply_to,
new_error(nil, jsonrpc.INVALID_REQUEST, "empty batch array"))
if not res then
ngx_log(ngx_WARN, "[rpc] unable to enqueue RPC error: ", err)
end
goto continue
end
-- batching rpc call
local collection = {}
for _, v in ipairs(payload) do
self:process_one_request(target_id, reply_to, v, collection)
end
if not isempty(collection) then
local res, err = self:_enqueue_rpc_response(reply_to, collection)
if not res then
ngx_log(ngx_WARN, "[rpc] unable to enqueue RPC call result: ", err)
end
end
::continue::
end -- for _, call
end -- if n.channel == rpc_resp_channel_name
end -- while true
local res, err = lconn:wait_for_notification()
if not res then
if not is_timeout(err) then
return nil, "wait_for_notification error: " .. err
end
repeat
local sql, err = self.sub_unsub:pop(0)
if err then
return nil, err
end
local _, notifications
res, err, _, notifications = lconn:query(sql or "SELECT 1;") -- keepalive
if not res then
return nil, "query to Postgres failed: " .. err
end
enqueue_notifications(notifications, notifications_queue)
until not sql
else
notifications_queue:push(res)
end
end -- while not exiting()
end
function _M:start(delay)
if not self.worker_id then
-- this can not be generated inside `:new()` as ngx.worker.id()
-- does not yet exist there and can only be generated inside
-- init_worker phase
self.worker_id = uuid.generate_v5(kong.node.get_id(),
tostring(ngx.worker.id()))
end
assert(ngx.timer.at(delay or 0, function(premature)
if premature then
return
end
local lconn = self.db.connector:connect("write")
lconn:settimeout(1000)
self.db.connector:store_connection(nil, "write")
local _, res_or_perr, err = pcall(self._event_loop, self, lconn)
-- _event_loop never returns true
local delay = math.random(5, 10)
ngx_log(ngx_ERR, "[rpc] concentrator event loop error: ",
res_or_perr or err, ", reconnecting in ",
math.floor(delay), " seconds")
local res, err = lconn:disconnect()
if not res then
ngx_log(ngx_ERR, "[rpc] unable to close postgres connection: ", err)
end
self:start(delay)
end))
end
-- enqueue a RPC request to DP node with ID node_id
function _M:_enqueue_rpc_request(node_id, payload)
local sql = string_format(RPC_REQUEST_ENQUEUE_SQL,
self.db.connector:escape_literal(node_id),
self.db.connector:escape_literal(self.worker_id),
5,
self.db.connector:escape_literal(cjson_encode(payload)),
self.db.connector:escape_literal(REQ_CHANNEL_PREFIX .. node_id))
return self.db.connector:query(sql)
end
-- enqueue a RPC response from CP worker with ID worker_id
-- collection is only for rpc batch call.
-- if collection is nil, it means the rpc is a single call.
function _M:_enqueue_rpc_response(worker_id, payload, collection)
if collection then
tb_insert(collection, payload)
return
end
local sql = string_format("SELECT pg_notify(%s, %s);",
self.db.connector:escape_literal(RESP_CHANNEL_PREFIX .. worker_id),
self.db.connector:escape_literal(cjson_encode(payload)))
return self.db.connector:query(sql)
end
-- subscribe to RPC calls for worker with ID node_id
function _M:_enqueue_subscribe(node_id)
return self.sub_unsub:push('LISTEN "' .. REQ_CHANNEL_PREFIX .. node_id .. '";')
end
-- unsubscribe to RPC calls for worker with ID node_id
function _M:_enqueue_unsubscribe(node_id)
return self.sub_unsub:push('UNLISTEN "' .. REQ_CHANNEL_PREFIX .. node_id .. '";')
end
-- asynchronously start executing a RPC, node_id is
-- needed for this implementation, because all nodes
-- over concentrator shares the same "socket" object
-- This way the manager code wouldn't tell the difference
-- between calls made over WebSocket or concentrator
function _M:call(node_id, method, params, callback)
local id
-- notification has no callback or id
if callback then
id = self:_get_next_id()
self.interest[id] = callback
end
return self:_enqueue_rpc_request(node_id, {
jsonrpc = jsonrpc.VERSION,
method = method,
params = params,
id = id,
})
end
return _M