kong--kong
406 行
11 KiB
Lua
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
|