Files
2026-07-13 12:32:21 +08:00

293 lines
8.1 KiB
Lua

---
--- manages a cache of upstream objects
--- and the relationship with healthcheckers and balancers
---
--- maybe it could eventually be merged into the DAO object?
---
---
---
local workspaces = require "kong.workspaces"
local constants = require "kong.constants"
local balancers
local healthcheckers
local ngx = ngx
local log = ngx.log
local null = ngx.null
local table_remove = table.remove
local timer_at = ngx.timer.at
local isempty = require("table.isempty")
local CRIT = ngx.CRIT
local DEBUG = ngx.DEBUG
local ERR = ngx.ERR
local GLOBAL_QUERY_OPTS = { workspace = null, show_ws_id = true }
local CLEAR_HEALTH_STATUS_DELAY = constants.CLEAR_HEALTH_STATUS_DELAY
local upstreams_M = {}
local upstream_by_name = {}
function upstreams_M.init()
balancers = require "kong.runloop.balancer.balancers"
healthcheckers = require "kong.runloop.balancer.healthcheckers"
upstream_by_name = {}
end
-- Caching logic
--
-- We retain 3 entities in cache:
--
-- 1) `"balancer:upstreams"` - a list of upstreams
-- to be invalidated on any upstream change
-- 2) `"balancer:upstreams:" .. id` - individual upstreams
-- to be invalidated on individual basis
-- 3) `"balancer:targets:" .. id`
-- target for an upstream along with the upstream it belongs to
--
-- Distinction between 1 and 2 makes it possible to invalidate individual
-- upstreams, instead of all at once forcing to rebuild all balancers
------------------------------------------------------------------------------
-- Loads a single upstream entity.
-- @param upstream_id string
-- @return the upstream table, or nil+error
local function load_upstream_into_memory(upstream_id)
local upstream, err = kong.db.upstreams:select({ id = upstream_id }, GLOBAL_QUERY_OPTS)
if not upstream then
return nil, err
end
return upstream
end
--_load_upstream_into_memory = load_upstream_into_memory
function upstreams_M.get_upstream_by_id(upstream_id)
local upstream_cache_key = "balancer:upstreams:" .. upstream_id
return kong.core_cache:get(upstream_cache_key, nil,
load_upstream_into_memory, upstream_id)
end
------------------------------------------------------------------------------
local function load_upstreams_dict_into_memory()
log(DEBUG, "loading upstreams dict into memory")
local upstreams_dict = {}
-- build a dictionary, indexed by the upstream name
local upstreams = kong.db.upstreams
local page_size
if upstreams.pagination then
page_size = upstreams.pagination.max_page_size
end
for up, err in upstreams:each(page_size, GLOBAL_QUERY_OPTS) do
if err then
log(CRIT, "could not obtain list of upstreams: ", err)
return nil, err
end
upstreams_dict[up.ws_id .. ":" .. up.name] = up.id
end
-- please refer to https://github.com/Kong/kong/pull/4301 and
-- https://github.com/Kong/kong/pull/8974#issuecomment-1317788871
if isempty(upstreams_dict) then
log(DEBUG, "no upstreams were specified")
end
return upstreams_dict
end
--_load_upstreams_dict_into_memory = load_upstreams_dict_into_memory
------------------------------------------------------------------------------
-- Implements a simple dictionary with all upstream-ids indexed
-- by their name.
-- @return The upstreams dictionary (a map with upstream names as string keys
-- and upstream entity tables as values), or nil+error
function upstreams_M.get_all_upstreams()
return kong.core_cache:get("balancer:upstreams", nil,
load_upstreams_dict_into_memory)
end
------------------------------------------------------------------------------
-- Finds and returns an upstream entity. This function covers
-- caching, invalidation, db access, et al.
-- @param upstream_name string.
-- @return upstream table, or `false` if not found, or nil+error
function upstreams_M.get_upstream_by_name(upstream_name)
local ws_id = workspaces.get_workspace_id()
local key = ws_id .. ":" .. upstream_name
if upstream_by_name[key] then
return upstream_by_name[key]
end
local upstreams_dict, err = upstreams_M.get_all_upstreams()
if err then
return nil, err
end
local upstream_id = upstreams_dict[key]
if not upstream_id then
return false -- no upstream by this name
end
local upstream, err = upstreams_M.get_upstream_by_id(upstream_id)
if err then
return nil, err
end
upstream_by_name[key] = upstream
return upstream
end
function upstreams_M.setUpstream_by_name(upstream)
local ws_id = upstream.ws_id or workspaces.get_workspace_id()
upstream_by_name[ws_id .. ":" .. upstream.name] = upstream
end
--==============================================================================
-- Event Callbacks
--==============================================================================
local upstream_events_queue = {}
local function do_upstream_event(operation, upstream_data)
local upstream_id = upstream_data.id
local upstream_name = upstream_data.name
local ws_id = upstream_data.ws_id or workspaces.get_workspace_id()
local by_name_key = ws_id .. ":" .. upstream_name
if operation == "create" then
local upstream, err = upstreams_M.get_upstream_by_id(upstream_id)
if err then
return nil, err
end
if not upstream then
log(ERR, "upstream not found for ", upstream_id)
return
end
local _, err = balancers.create_balancer(upstream)
if err then
log(CRIT, "failed creating balancer for ", upstream_name, ": ", err)
end
elseif operation == "delete" or operation == "update" then
local balancer = balancers.get_balancer_by_id(upstream_id)
if balancer then
healthcheckers.stop_healthchecker(balancer, CLEAR_HEALTH_STATUS_DELAY)
end
if operation == "delete" then
balancers.set_balancer(upstream_id, nil)
upstream_by_name[by_name_key] = nil
else
local upstream = upstreams_M.get_upstream_by_id(upstream_id)
if not upstream then
log(ERR, "upstream not found for ", upstream_id)
return
end
local _, err = balancers.create_balancer(upstream, true)
if err then
log(ERR, "failed recreating balancer for ", upstream_name, ": ", err)
end
end
end
end
local function set_upstream_events_queue(operation, upstream_data)
-- insert the new event into the end of the queue
upstream_events_queue[#upstream_events_queue + 1] = {
operation = operation,
upstream_data = upstream_data,
}
end
local update_balancer_state_running
local function update_balancer_state_timer(premature)
if premature then
return
end
update_balancer_state_running = true
while upstream_events_queue[1] do
local event = upstream_events_queue[1]
local _, err = do_upstream_event(event.operation, event.upstream_data)
if err then
log(CRIT, "failed handling upstream event: ", err)
return
end
table_remove(upstream_events_queue, 1)
end
local frequency = kong.configuration.worker_state_update_frequency or 1
local _, err = timer_at(frequency, update_balancer_state_timer)
if err then
update_balancer_state_running = false
log(CRIT, "unable to reschedule update proxy state timer: ", err)
end
end
function upstreams_M.update_balancer_state()
if update_balancer_state_running then
return
end
local frequency = kong.configuration.worker_state_update_frequency or 1
local _, err = timer_at(frequency, update_balancer_state_timer)
if err then
log(CRIT, "unable to start update proxy state timer: ", err)
else
update_balancer_state_running = true
log(DEBUG, "update proxy state timer scheduled")
end
end
--------------------------------------------------------------------------------
-- Called on any changes to an upstream.
-- @param operation "create", "update" or "delete"
-- @param upstream_data table with `id` and `name` fields
function upstreams_M.on_upstream_event(operation, upstream_data)
if kong.configuration.worker_consistency == "strict" then
local _, err = do_upstream_event(operation, upstream_data)
if err then
log(CRIT, "failed handling upstream event: ", err)
end
else
set_upstream_events_queue(operation, upstream_data)
end
end
return upstreams_M