kong--kong
379 行
9.1 KiB
Lua
379 行
9.1 KiB
Lua
local ngx_debug = ngx.config.debug
|
|
local DEBUG = ngx.DEBUG
|
|
local ERR = ngx.ERR
|
|
local CRIT = ngx.CRIT
|
|
local max = math.max
|
|
local type = type
|
|
local error = error
|
|
local pcall = pcall
|
|
local insert = table.insert
|
|
local ngx_log = ngx.log
|
|
local ngx_now = ngx.now
|
|
local timer_at = ngx.timer.at
|
|
local now_updated = require("kong.tools.time").get_updated_now
|
|
|
|
local knode = kong and kong.node or require "kong.pdk.node".new()
|
|
local concurrency = require "kong.concurrency"
|
|
|
|
local POLL_INTERVAL_LOCK_KEY = "cluster_events:poll_interval"
|
|
local POLL_RUNNING_LOCK_KEY = "cluster_events:poll_running"
|
|
local CURRENT_AT_KEY = "cluster_events:at"
|
|
|
|
|
|
local MIN_EVENT_TTL_IN_DB = 60 * 60 -- 1 hour
|
|
local PAGE_SIZE = 1000
|
|
|
|
|
|
local _init
|
|
local poll_handler
|
|
|
|
|
|
local function log(lvl, ...)
|
|
return ngx_log(lvl, "[cluster events] ", ...)
|
|
end
|
|
|
|
|
|
local function nbf_cb_handler(premature, cb, data)
|
|
if premature then
|
|
return
|
|
end
|
|
|
|
cb(data)
|
|
end
|
|
|
|
|
|
-- module
|
|
|
|
|
|
local _M = {}
|
|
local mt = { __index = _M }
|
|
|
|
|
|
function _M.new(opts)
|
|
if ngx.get_phase() ~= "init_worker" and ngx.get_phase() ~= "timer" then
|
|
return error("kong.cluster_events must be created during init_worker phase")
|
|
end
|
|
|
|
if not ngx_debug and _init then
|
|
return error("kong.cluster_events was already instantiated")
|
|
end
|
|
|
|
-- opts validations
|
|
|
|
opts = opts or {}
|
|
|
|
if opts.poll_interval and type(opts.poll_interval) ~= "number" then
|
|
return error("opts.poll_interval must be a number")
|
|
end
|
|
|
|
if opts.poll_offset and type(opts.poll_offset) ~= "number" then
|
|
return error("opts.poll_offset must be a number")
|
|
end
|
|
|
|
if opts.poll_delay and type(opts.poll_delay) ~= "number" then
|
|
return error("opts.poll_delay must be a number")
|
|
end
|
|
|
|
if not opts.db then
|
|
return error("opts.db is required")
|
|
end
|
|
|
|
-- strategy selection
|
|
|
|
local strategy
|
|
local poll_interval = max(opts.poll_interval or 5, 0)
|
|
local poll_offset = max(opts.poll_offset or 0, 0)
|
|
local poll_delay = max(opts.poll_delay or 0, 0)
|
|
|
|
do
|
|
local db_strategy
|
|
|
|
if opts.db.strategy == "postgres" then
|
|
db_strategy = require "kong.cluster_events.strategies.postgres"
|
|
|
|
elseif opts.db.strategy == "off" then
|
|
db_strategy = require "kong.cluster_events.strategies.off"
|
|
|
|
else
|
|
return error("no cluster_events strategy for " ..
|
|
opts.db.strategy)
|
|
end
|
|
|
|
local event_ttl_in_db = max(poll_offset * 10, MIN_EVENT_TTL_IN_DB)
|
|
|
|
strategy = db_strategy.new(opts.db, PAGE_SIZE, event_ttl_in_db)
|
|
end
|
|
|
|
-- instantiation
|
|
|
|
local self = {
|
|
shm = ngx.shared.kong,
|
|
events_shm = ngx.shared.kong_cluster_events,
|
|
strategy = strategy,
|
|
poll_interval = poll_interval,
|
|
poll_offset = poll_offset,
|
|
poll_delay = poll_delay,
|
|
event_ttl_shm = poll_interval * 2 + poll_offset,
|
|
node_id = nil,
|
|
polling = false,
|
|
channels = {},
|
|
callbacks = {},
|
|
use_polling = strategy:should_use_polling(),
|
|
}
|
|
|
|
-- set current time (at)
|
|
|
|
local now = strategy:server_time() or ngx_now()
|
|
local ok, err = self.shm:safe_set(CURRENT_AT_KEY, now)
|
|
if not ok then
|
|
return nil, "failed to set 'at' in shm: " .. err
|
|
end
|
|
|
|
-- set node id (uuid)
|
|
self.node_id, err = knode.get_id()
|
|
if not self.node_id then
|
|
return nil, err
|
|
end
|
|
|
|
if ngx_debug and opts.node_id then
|
|
self.node_id = opts.node_id
|
|
end
|
|
|
|
_init = true
|
|
|
|
return setmetatable(self, mt)
|
|
end
|
|
|
|
|
|
function _M:broadcast(channel, data, delay)
|
|
if type(channel) ~= "string" then
|
|
return nil, "channel must be a string"
|
|
end
|
|
|
|
if type(data) ~= "string" then
|
|
return nil, "data must be a string"
|
|
end
|
|
|
|
if delay and type(delay) ~= "number" then
|
|
return nil, "delay must be a number"
|
|
|
|
elseif self.poll_delay > 0 then
|
|
delay = self.poll_delay
|
|
end
|
|
|
|
-- insert event row
|
|
|
|
--log(DEBUG, "broadcasting on channel: '", channel, "' data: ", data,
|
|
-- " with delay: ", delay and delay or "none")
|
|
|
|
local ok, err = self.strategy:insert(self.node_id, channel, nil, data, delay)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
function _M:subscribe(channel, cb, start_polling)
|
|
if type(channel) ~= "string" then
|
|
return error("channel must be a string")
|
|
end
|
|
|
|
if type(cb) ~= "function" then
|
|
return error("callback must be a function")
|
|
end
|
|
|
|
if not self.callbacks[channel] then
|
|
self.callbacks[channel] = { cb }
|
|
|
|
insert(self.channels, channel)
|
|
|
|
else
|
|
insert(self.callbacks[channel], cb)
|
|
end
|
|
|
|
if start_polling == nil then
|
|
start_polling = true
|
|
end
|
|
|
|
if not self.polling and start_polling and self.use_polling then
|
|
-- start recurring polling timer
|
|
|
|
local ok, err = timer_at(self.poll_interval, poll_handler, self)
|
|
if not ok then
|
|
return nil, "failed to start polling timer: " .. err
|
|
end
|
|
|
|
self.polling = true
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local function process_event(self, row, local_start_time)
|
|
if row.node_id == self.node_id then
|
|
return true
|
|
end
|
|
|
|
local ran, err = self.events_shm:get(row.id)
|
|
if err then
|
|
return nil, "failed to probe if event ran: " .. err
|
|
end
|
|
|
|
if ran then
|
|
return true
|
|
end
|
|
|
|
log(DEBUG, "new event (channel: '", row.channel, "') data: '", row.data,
|
|
"' nbf: '", row.nbf or "none", "' shm exptime: ",
|
|
self.event_ttl_shm)
|
|
|
|
-- mark as ran before running in case of long-running callbacks
|
|
local ok, err = self.events_shm:set(row.id, true, self.event_ttl_shm)
|
|
if not ok then
|
|
return nil, "failed to mark event as ran: " .. err
|
|
end
|
|
|
|
local cbs = self.callbacks[row.channel]
|
|
if not cbs then
|
|
return true
|
|
end
|
|
|
|
for j = 1, #cbs do
|
|
local delay
|
|
|
|
if row.nbf and row.now then
|
|
local now = row.now + max(now_updated() - local_start_time, 0)
|
|
delay = max(row.nbf - now, 0)
|
|
end
|
|
|
|
if delay and delay > 0 then
|
|
log(DEBUG, "delaying nbf event by ", delay, "s")
|
|
|
|
local ok, err = timer_at(delay, nbf_cb_handler, cbs[j], row.data)
|
|
if not ok then
|
|
log(ERR, "failed to schedule nbf event timer: ", err)
|
|
end
|
|
|
|
else
|
|
local ok, err = pcall(cbs[j], row.data)
|
|
if not ok and not ngx_debug then
|
|
log(ERR, "callback threw an error: ", err)
|
|
end
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local prev_min_at
|
|
|
|
local function poll(self)
|
|
-- get events since last poll
|
|
|
|
local min_at, err = self.shm:get(CURRENT_AT_KEY)
|
|
if err then
|
|
return nil, "failed to retrieve 'at' in shm: " .. err
|
|
end
|
|
|
|
if min_at then
|
|
-- apply grace period
|
|
min_at = min_at - self.poll_offset - 0.001
|
|
if min_at ~= prev_min_at then
|
|
prev_min_at = min_at
|
|
log(DEBUG, "polling events from: ", min_at)
|
|
end
|
|
|
|
else
|
|
-- 'at' was evicted from 'kong' shm - safest is to resume fetching events
|
|
-- that may still be in the shm to ensure that we do not replay them
|
|
-- This is far from normal behavior, since the 'at' value should never
|
|
-- be evicted from the 'kong' shm (which should be frozen and never subject
|
|
-- to eviction, unless misused).
|
|
local now = self.strategy:server_time() or ngx_now()
|
|
min_at = now - self.event_ttl_shm
|
|
log(CRIT, "no 'at' in shm, polling events from: ", min_at)
|
|
end
|
|
|
|
for rows, err, page in self.strategy:select_interval(self.channels, min_at) do
|
|
if err then
|
|
return nil, "failed to retrieve events from DB: " .. err
|
|
end
|
|
|
|
local count = #rows
|
|
|
|
if page == 1 and rows[1].now then
|
|
local ok, err = self.shm:safe_set(CURRENT_AT_KEY, rows[1].now)
|
|
if not ok then
|
|
return nil, "failed to set 'at' in shm: " .. err
|
|
end
|
|
end
|
|
|
|
local local_start_time = now_updated()
|
|
for i = 1, count do
|
|
local ok, err = process_event(self, rows[i], local_start_time)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
if ngx_debug then
|
|
_M.poll = poll
|
|
end
|
|
|
|
|
|
poll_handler = function(premature, self)
|
|
if premature or not self.polling then
|
|
-- set self.polling to false to stop a polling loop
|
|
return
|
|
end
|
|
|
|
-- check if a poll is not currently running, to ensure we don't start
|
|
-- another poll while a worker is still stuck in its own polling (in
|
|
-- case it is being slow)
|
|
-- we still add an exptime to this lock in case something goes horribly
|
|
-- wrong, to ensure other workers can poll new events
|
|
-- a poll cannot take more than max(poll_interval * 5, 10) -- 10s min
|
|
local ok, err = concurrency.with_worker_mutex({
|
|
name = POLL_RUNNING_LOCK_KEY,
|
|
timeout = 0,
|
|
exptime = max(self.poll_interval * 5, 10),
|
|
}, function()
|
|
if self.poll_interval > 0.001 then
|
|
-- check if interval of `poll_interval` has elapsed already, to ensure
|
|
-- we do not run the poll when a previous poll was quickly executed, but
|
|
-- another worker got the timer trigger a bit too late.
|
|
return concurrency.with_worker_mutex({
|
|
name = POLL_INTERVAL_LOCK_KEY,
|
|
timeout = 0,
|
|
exptime = self.poll_interval - 0.001,
|
|
}, function()
|
|
return poll(self)
|
|
end)
|
|
end
|
|
|
|
return poll(self)
|
|
end)
|
|
|
|
if not ok and err ~= "timeout" then
|
|
log(ERR, err)
|
|
end
|
|
|
|
-- schedule next polling timer
|
|
ok, err = timer_at(self.poll_interval, poll_handler, self)
|
|
if not ok then
|
|
log(CRIT, "failed to start recurring polling timer: ", err)
|
|
end
|
|
end
|
|
|
|
|
|
return _M
|