kong--kong
552 行
15 KiB
Lua
552 行
15 KiB
Lua
local _M = {}
|
|
local _MT = { __index = _M, }
|
|
|
|
|
|
local txn = require("resty.lmdb.transaction")
|
|
local declarative = require("kong.db.declarative")
|
|
local constants = require("kong.constants")
|
|
local concurrency = require("kong.concurrency")
|
|
local isempty = require("table.isempty")
|
|
local events = require("kong.runloop.events")
|
|
local EMPTY = require("kong.tools.table").EMPTY
|
|
|
|
|
|
local validate_deltas = require("kong.clustering.services.sync.validate").validate_deltas
|
|
local insert_entity_for_txn = declarative.insert_entity_for_txn
|
|
local delete_entity_for_txn = declarative.delete_entity_for_txn
|
|
local DECLARATIVE_HASH_KEY = constants.DECLARATIVE_HASH_KEY
|
|
local CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY = constants.CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY
|
|
local DECLARATIVE_EMPTY_CONFIG_HASH = constants.DECLARATIVE_EMPTY_CONFIG_HASH
|
|
local DECLARATIVE_DEFAULT_WORKSPACE_KEY = constants.DECLARATIVE_DEFAULT_WORKSPACE_KEY
|
|
local CLUSTERING_SYNC_STATUS = constants.CLUSTERING_SYNC_STATUS
|
|
local SYNC_MUTEX_OPTS = { name = "get_delta", timeout = 0, }
|
|
local GLOBAL_QUERY_OPTS = { show_ws_id = true, }
|
|
local MAX_RETRY = 5
|
|
|
|
|
|
local assert = assert
|
|
local ipairs = ipairs
|
|
local ngx_null = ngx.null
|
|
local ngx_log = ngx.log
|
|
local ngx_ERR = ngx.ERR
|
|
local ngx_WARN = ngx.WARN
|
|
local ngx_INFO = ngx.INFO
|
|
local ngx_DEBUG = ngx.DEBUG
|
|
|
|
|
|
function _M.new(strategy)
|
|
local self = {
|
|
strategy = strategy,
|
|
}
|
|
|
|
return setmetatable(self, _MT)
|
|
end
|
|
|
|
|
|
local function empty_sync_result()
|
|
return { default = { deltas = {}, full_sync = false, }, }
|
|
end
|
|
|
|
|
|
local function full_sync_result()
|
|
local deltas, err = declarative.export_config_sync()
|
|
if not deltas then
|
|
return nil, err
|
|
end
|
|
|
|
-- wipe dp lmdb, full sync
|
|
return { default = { deltas = deltas, full_sync = true, }, }
|
|
end
|
|
|
|
|
|
local function get_current_version()
|
|
return declarative.get_current_hash() or DECLARATIVE_EMPTY_CONFIG_HASH
|
|
end
|
|
|
|
|
|
function _M:init_cp(manager)
|
|
local purge_delay = manager.conf.cluster_data_plane_purge_delay
|
|
|
|
-- CP
|
|
-- Method: kong.sync.v2.notify_validation_error
|
|
-- Params: msg: error message reported by DP
|
|
-- example: { version = <latest version of deltas>, error = <flatten error>, }
|
|
manager.callbacks:register("kong.sync.v2.notify_validation_error", function(node_id, msg)
|
|
ngx_log(ngx_DEBUG, "[kong.sync.v2] received validation error")
|
|
-- TODO: We need a better error handling method, it might report this error
|
|
-- to Konnect or or log it locally.
|
|
return true
|
|
end)
|
|
|
|
-- CP
|
|
-- Method: kong.sync.v2.get_delta
|
|
-- Params: versions: list of current versions of the database
|
|
-- example: { default = { version = "1000", }, }
|
|
manager.callbacks:register("kong.sync.v2.get_delta", function(node_id, current_versions)
|
|
kong.log.trace("[kong.sync.v2] config push (connected client)")
|
|
|
|
local rpc_peers
|
|
if kong.rpc then
|
|
rpc_peers = kong.rpc:get_peers()
|
|
end
|
|
|
|
local default_namespace = current_versions.default
|
|
|
|
if not default_namespace then
|
|
return nil, "default namespace does not exist inside params"
|
|
end
|
|
|
|
-- { default = { version = "1000", }, }
|
|
local default_namespace_version = default_namespace.version
|
|
local node_info = assert(kong.rpc:get_peer_info(node_id))
|
|
|
|
-- follow update_sync_status() in control_plane.lua
|
|
local ok, err = kong.db.clustering_data_planes:upsert({ id = node_id }, {
|
|
last_seen = ngx.time(),
|
|
hostname = node_id,
|
|
ip = node_info.ip, -- get the correct ip
|
|
version = node_info.version, -- get from rpc call
|
|
labels = node_info.labels, -- get from rpc call
|
|
cert_details = node_info.cert_details, -- get from rpc call
|
|
sync_status = CLUSTERING_SYNC_STATUS.NORMAL,
|
|
config_hash = default_namespace_version,
|
|
rpc_capabilities = rpc_peers and rpc_peers[node_id] or EMPTY,
|
|
}, { ttl = purge_delay, no_broadcast_crud_event = true, })
|
|
if not ok then
|
|
ngx_log(ngx_ERR, "unable to update clustering data plane status: ", err)
|
|
end
|
|
|
|
local latest_version, err = self.strategy:get_latest_version()
|
|
if not latest_version then
|
|
return nil, err
|
|
end
|
|
|
|
-- string comparison effectively does the same as number comparison
|
|
if not self.strategy:is_valid_version(default_namespace_version) or
|
|
default_namespace_version ~= latest_version then
|
|
return full_sync_result()
|
|
end
|
|
|
|
return empty_sync_result()
|
|
end)
|
|
end
|
|
|
|
|
|
function _M:init_dp(manager)
|
|
local kong_shm = ngx.shared.kong
|
|
-- DP
|
|
-- Method: kong.sync.v2.notify_new_version
|
|
-- Params: new_versions: list of namespaces and their new versions, like:
|
|
-- { default = { new_version = "1000", }, }
|
|
manager.callbacks:register("kong.sync.v2.notify_new_version", function(node_id, new_versions)
|
|
-- TODO: currently only default is supported, and anything else is ignored
|
|
local default_new_version = new_versions.default
|
|
if not default_new_version then
|
|
return nil, "default namespace does not exist inside params"
|
|
end
|
|
|
|
local version = default_new_version.new_version
|
|
if not version then
|
|
return nil, "'new_version' key does not exist"
|
|
end
|
|
|
|
local lmdb_ver = get_current_version()
|
|
if lmdb_ver < version then
|
|
-- set lastest version to shm
|
|
kong_shm:set(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY, version)
|
|
return self:sync_once()
|
|
end
|
|
|
|
ngx_log(ngx_DEBUG, "no sync runs, version is ", version)
|
|
|
|
return true
|
|
end)
|
|
end
|
|
|
|
|
|
function _M:init(manager, is_cp)
|
|
if is_cp then
|
|
self:init_cp(manager)
|
|
else
|
|
self:init_dp(manager)
|
|
end
|
|
end
|
|
|
|
|
|
-- check if rpc connection is ready
|
|
local function is_rpc_ready()
|
|
-- TODO: find a better way to detect when the RPC layer, including caps list,
|
|
-- has been fully initialized, instead of waiting for up to 5.5 seconds
|
|
for i = 1, 10 do
|
|
local res = kong.rpc:get_peers()
|
|
|
|
-- control_plane is ready
|
|
if res["control_plane"] then
|
|
return true
|
|
end
|
|
|
|
-- retry later
|
|
ngx.sleep(0.1 * i)
|
|
end
|
|
end
|
|
|
|
|
|
-- tell cp that the deltas validation failed
|
|
local function notify_error(ver, err_t)
|
|
local msg = {
|
|
version = ver or "v02_deltas_have_no_latest_version_field",
|
|
error = err_t,
|
|
}
|
|
|
|
local ok, err = kong.rpc:notify("control_plane",
|
|
"kong.sync.v2.notify_validation_error",
|
|
msg)
|
|
if not ok then
|
|
ngx_log(ngx_ERR, "notifying validation errors failed: ", err)
|
|
end
|
|
end
|
|
|
|
|
|
-- tell cp we already updated the version by rpc notification
|
|
local function update_status(ver)
|
|
local msg = { default = { version = ver, }, }
|
|
|
|
local ok, err = kong.rpc:notify("control_plane", "kong.sync.v2.get_delta", msg)
|
|
if not ok then
|
|
ngx_log(ngx_ERR, "update status notification failed: ", err)
|
|
end
|
|
end
|
|
|
|
|
|
local function lmdb_update(db, t, delta, is_full_sync)
|
|
local delta_type = delta.type
|
|
local delta_entity = delta.entity
|
|
|
|
-- upsert the entity
|
|
-- delete if exists
|
|
local old_entity, err = db[delta_type]:select(delta_entity, GLOBAL_QUERY_OPTS)
|
|
if err then
|
|
return nil, err
|
|
end
|
|
|
|
if old_entity and not is_full_sync then
|
|
local res, err = delete_entity_for_txn(t, delta_type, old_entity)
|
|
if not res then
|
|
return nil, err
|
|
end
|
|
end
|
|
|
|
local res, err = insert_entity_for_txn(t, delta_type, delta_entity)
|
|
if not res then
|
|
return nil, err
|
|
end
|
|
|
|
if is_full_sync then
|
|
return nil
|
|
end
|
|
|
|
return { delta_type, old_entity and "update" or "create", delta_entity, old_entity, }
|
|
end
|
|
|
|
|
|
local function lmdb_delete(db, t, delta, is_full_sync)
|
|
local delta_type = delta.type
|
|
-- The show_ws_id option ensures that the old_entity contains the ws_id field.
|
|
local opts = { workspace = delta.ws_id, show_ws_id = true, }
|
|
|
|
local old_entity, err = db[delta_type]:select(delta.pk, opts)
|
|
if err then
|
|
return nil, err
|
|
end
|
|
|
|
-- full sync requires extra torlerance for missing entities
|
|
if not old_entity then
|
|
return nil
|
|
end
|
|
|
|
local res, err = delete_entity_for_txn(t, delta_type, old_entity)
|
|
if not res then
|
|
return nil, err
|
|
end
|
|
|
|
if is_full_sync then
|
|
return nil
|
|
end
|
|
|
|
return { delta_type, "delete", old_entity, }
|
|
end
|
|
|
|
|
|
local function preprocess_deltas(deltas)
|
|
local default_ws_changed
|
|
|
|
for _, delta in ipairs(deltas) do
|
|
local delta_type = delta.type
|
|
local delta_entity = delta.entity
|
|
|
|
-- Update default workspace if delta is for workspace update
|
|
if delta_type == "workspaces" and
|
|
delta_entity ~= nil and
|
|
delta_entity ~= ngx_null and
|
|
delta_entity.name == "default" and
|
|
kong.default_workspace ~= delta_entity.id
|
|
then
|
|
kong.default_workspace = delta_entity.id
|
|
default_ws_changed = true
|
|
break
|
|
end
|
|
end -- for _, delta
|
|
|
|
assert(type(kong.default_workspace) == "string")
|
|
|
|
return default_ws_changed
|
|
end
|
|
|
|
|
|
local function do_sync()
|
|
if not is_rpc_ready() then
|
|
return nil, "rpc is not ready"
|
|
end
|
|
|
|
local current_version = get_current_version()
|
|
local msg = { default = { version = current_version, }, }
|
|
|
|
local ns_deltas, err = kong.rpc:call("control_plane", "kong.sync.v2.get_delta", msg)
|
|
if not ns_deltas then
|
|
ngx_log(ngx_ERR, "sync get_delta error: ", err)
|
|
return true
|
|
end
|
|
|
|
-- ns_deltas should look like:
|
|
-- { default = { deltas = { ... }, full_sync = true, }, }
|
|
|
|
local ns_delta = ns_deltas.default
|
|
if not ns_delta then
|
|
return nil, "default namespace does not exist inside params"
|
|
end
|
|
|
|
local is_full_sync = ns_delta.full_sync or ns_delta.wipe
|
|
local deltas = ns_delta.deltas
|
|
|
|
if not deltas then
|
|
return nil, "sync get_delta error: deltas is null"
|
|
end
|
|
|
|
if isempty(deltas) then
|
|
-- no delta to sync
|
|
return true
|
|
end
|
|
|
|
-- we should find the correct default workspace
|
|
-- and replace the old one with it
|
|
local default_ws_changed = preprocess_deltas(deltas)
|
|
|
|
-- validate deltas and set the default values
|
|
local ok, err, err_t = validate_deltas(deltas, is_full_sync)
|
|
if not ok then
|
|
notify_error(ns_delta.latest_version, err_t)
|
|
return nil, err
|
|
end
|
|
|
|
local t = txn.begin(512)
|
|
|
|
if is_full_sync then
|
|
ngx_log(ngx_INFO, "[kong.sync.v2] full sync begins")
|
|
|
|
t:db_drop(false)
|
|
end
|
|
|
|
local db = kong.db
|
|
|
|
local version = current_version
|
|
local crud_events = {}
|
|
local crud_events_n = 0
|
|
|
|
-- delta should look like:
|
|
-- { type = ..., entity = { ... }, version = "1", ws_id = ..., }
|
|
for _, delta in ipairs(deltas) do
|
|
local delta_version = delta.version
|
|
local delta_type = delta.type
|
|
local delta_entity = delta.entity
|
|
|
|
local is_update = delta_entity ~= nil and delta_entity ~= ngx_null
|
|
local operation_name = is_update and "update" or "delete"
|
|
local operation = is_update and lmdb_update or lmdb_delete
|
|
|
|
-- log the operation before executing it, so when failing we know what entity caused it
|
|
ngx_log(ngx_DEBUG,
|
|
"[kong.sync.v2] ", operation_name, " entity",
|
|
", version: ", delta_version,
|
|
", type: ", delta_type)
|
|
|
|
local ev, err = operation(db, t, delta, is_full_sync)
|
|
if err then
|
|
return nil, err
|
|
end
|
|
|
|
if ev then
|
|
crud_events_n = crud_events_n + 1
|
|
crud_events[crud_events_n] = ev
|
|
end
|
|
|
|
-- delta.version should not be nil or ngx.null
|
|
assert(type(delta_version) == "string")
|
|
|
|
if delta_version ~= version then
|
|
version = delta_version
|
|
end
|
|
end -- for _, delta
|
|
|
|
-- store current sync version
|
|
t:set(DECLARATIVE_HASH_KEY, version)
|
|
|
|
-- record the default workspace into LMDB for any of the following case:
|
|
-- * the default workspace has been changed
|
|
-- * full sync
|
|
if default_ws_changed or is_full_sync then
|
|
t:set(DECLARATIVE_DEFAULT_WORKSPACE_KEY, kong.default_workspace)
|
|
end
|
|
|
|
local ok, err = t:commit()
|
|
if not ok then
|
|
return nil, "failed to commit transaction: " .. err
|
|
end
|
|
|
|
if is_full_sync then
|
|
ngx_log(ngx_INFO, "[kong.sync.v2] full sync ends")
|
|
|
|
kong.core_cache:purge()
|
|
kong.cache:purge()
|
|
|
|
-- Trigger other workers' callbacks like reconfigure_handler.
|
|
--
|
|
-- Full sync could rebuild route, plugins and balancer route, so their
|
|
-- hashes are nil.
|
|
local reconfigure_data = { kong.default_workspace, nil, nil, nil, }
|
|
return events.declarative_reconfigure_notify(reconfigure_data)
|
|
end
|
|
|
|
for _, event in ipairs(crud_events) do
|
|
-- delta_type, crud_event_type, delta.entity, old_entity
|
|
db[event[1]]:post_crud_event(event[2], event[3], event[4])
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local function sync_handler(premature)
|
|
if premature then
|
|
return
|
|
end
|
|
|
|
local res, err = concurrency.with_worker_mutex(SYNC_MUTEX_OPTS, do_sync)
|
|
if not res and err ~= "timeout" then
|
|
ngx_log(ngx_ERR, "unable to create worker mutex and sync: ", err)
|
|
end
|
|
|
|
return res, err
|
|
end
|
|
|
|
|
|
local function sync_once_impl(premature, retry_count)
|
|
if premature then
|
|
return
|
|
end
|
|
|
|
local version_before_sync = get_current_version()
|
|
|
|
local _, err = sync_handler()
|
|
|
|
-- check if "kong.sync.v2.notify_new_version" updates the latest version
|
|
|
|
local latest_notified_version = ngx.shared.kong:get(CLUSTERING_DATA_PLANES_LATEST_VERSION_KEY)
|
|
if not latest_notified_version then
|
|
ngx_log(ngx_DEBUG, "no version notified yet")
|
|
return
|
|
end
|
|
|
|
local current_version = get_current_version()
|
|
if current_version >= latest_notified_version then
|
|
ngx_log(ngx_DEBUG, "version already updated")
|
|
|
|
-- version changed, we should update status
|
|
if version_before_sync ~= current_version then
|
|
update_status(current_version)
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
-- retry if the version is not updated
|
|
retry_count = retry_count or 0
|
|
|
|
if retry_count >= MAX_RETRY then
|
|
ngx_log(ngx_WARN, "sync_once retry count exceeded. retry_count: ", retry_count)
|
|
return
|
|
end
|
|
|
|
-- we do not count a timed out sync. just retry
|
|
if err ~= "timeout" then
|
|
retry_count = retry_count + 1
|
|
end
|
|
|
|
-- in some cases, the new spawned timer will be switched to immediately,
|
|
-- preventing the coroutine who possesses the mutex to run
|
|
-- to let other coroutines has a chance to run
|
|
local ok, err = kong.timer:at(0.1, sync_once_impl, retry_count)
|
|
-- this is a workaround for a timerng bug, where tail recursion causes failure
|
|
-- ok could be a string so let's convert it to boolean
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
return true
|
|
end
|
|
|
|
|
|
function _M:sync_once(delay)
|
|
local name = "rpc_sync_v2_once"
|
|
local is_managed = kong.timer:is_managed(name)
|
|
|
|
-- we are running a sync handler
|
|
if is_managed then
|
|
return true
|
|
end
|
|
|
|
local ok, err = kong.timer:named_at(name, delay or 0, sync_once_impl, 0)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
function _M:sync_every(delay, stop)
|
|
local name = "rpc_sync_v2_every"
|
|
local is_managed = kong.timer:is_managed(name)
|
|
|
|
-- we only start or stop once
|
|
|
|
if stop then
|
|
if is_managed then
|
|
assert(kong.timer:cancel(name))
|
|
end
|
|
return true
|
|
end
|
|
|
|
if is_managed then
|
|
return true
|
|
end
|
|
|
|
local ok, err = kong.timer:named_every(name, delay, sync_handler)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
return _M
|