Files
kong--kong/spec/01-unit/27-queue_spec.lua
T
2026-07-13 12:32:21 +08:00

881 lines
25 KiB
Lua

local Queue = require "kong.tools.queue"
local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy
local helpers = require "spec.helpers"
local mocker = require "spec.fixtures.mocker"
local timerng = require "resty.timerng"
local queue_schema = require "kong.tools.queue_schema"
local uuid = require("kong.tools.uuid").uuid
local queue_num = 1
local function queue_conf(conf)
local defaulted_conf = cycle_aware_deep_copy(conf)
if not conf.name then
defaulted_conf.name = "test-" .. tostring(queue_num)
queue_num = queue_num + 1
end
for _, field in ipairs(queue_schema.fields) do
for name, attrs in pairs(field) do
defaulted_conf[name] = conf[name] or attrs.default
end
end
return defaulted_conf
end
local function wait_until_queue_done(name)
helpers.wait_until(function()
return not Queue._exists(name)
end, 10)
end
describe("plugin queue", function()
lazy_setup(function()
kong.timer = timerng.new()
kong.timer:start()
-- make sure our workspace is explicitly set so that we test behavior in the presence of workspaces
ngx.ctx.workspace = "queue-tests"
end)
lazy_teardown(function()
kong.timer:destroy()
ngx.ctx.workspace = nil
end)
local unmock
local now_offset
local log_messages
local function count_matching_log_messages(s)
return select(2, string.gsub(log_messages, s, ""))
end
before_each(function()
local real_now = ngx.now
now_offset = 0
log_messages = ""
local function log(level, message) -- luacheck: ignore
log_messages = log_messages .. level .. " " .. message .. "\n"
end
mocker.setup(function(f)
unmock = f
end, {
kong = {
log = {
trace = function(message) return log('DEBUG', message) end,
debug = function(message) return log('DEBUG', message) end,
info = function(message) return log('INFO', message) end,
warn = function(message) return log('WARN', message) end,
err = function(message) return log('ERR', message) end,
},
plugin = {
get_id = function () return uuid() end,
},
},
ngx = {
ctx = {
-- make sure our workspace is nil to begin with to prevent leakage from
-- other tests
workspace = nil
},
-- We want to be able to fake the time returned by ngx.now() only in the queue module and leave everything
-- else alone so that we can see what effects changing the system time has on queues.
now = function()
local called_from = debug.getinfo(2, "nSl")
if string.match(called_from.short_src, "/queue.lua$") then
return real_now() + now_offset
else
return real_now()
end
end,
worker = {
exiting = function()
return false
end
}
}
})
end)
after_each(unmock)
it("passes configuration to handler", function ()
local handler_invoked
local configuration_sent = { foo = "bar" }
local configuration_received
Queue.enqueue(
queue_conf({ name = "handler-configuration" }),
function (conf)
handler_invoked = true
configuration_received = conf
return true
end,
configuration_sent,
"ENTRY"
)
wait_until_queue_done("handler-configuration")
helpers.wait_until(
function ()
if handler_invoked then
assert.same(configuration_sent, configuration_received)
return true
end
end,
10)
end)
it("displays log_tag in log entries", function ()
local handler_invoked
local log_tag = uuid()
Queue.enqueue(
queue_conf({ name = "log-tag", log_tag = log_tag }),
function ()
handler_invoked = true
return true
end,
nil,
"ENTRY"
)
wait_until_queue_done("handler-configuration")
helpers.wait_until(
function ()
if handler_invoked then
return true
end
end,
10)
assert.match_re(log_messages, "" .. log_tag .. ".*done processing queue")
end)
it("configuration changes are observed for older entries", function ()
local handler_invoked
local first_configuration_sent = { foo = "bar" }
local second_configuration_sent = { foo = "bar" }
local configuration_received
local number_of_entries_received
local function enqueue(conf, entry)
Queue.enqueue(
queue_conf({
name = "handler-configuration-change",
max_batch_size = 10,
max_coalescing_delay = 0.1
}),
function (c, entries)
handler_invoked = true
configuration_received = c
number_of_entries_received = #entries
return true
end,
conf,
entry
)
end
enqueue(first_configuration_sent, "ENTRY1")
enqueue(second_configuration_sent, "ENTRY2")
wait_until_queue_done("handler-configuration-change")
helpers.wait_until(
function ()
if handler_invoked then
assert.same(configuration_received, second_configuration_sent)
assert.equals(2, number_of_entries_received)
return true
end
end,
10)
end)
it("does not batch messages when `max_batch_size` is 1", function()
local process_count = 0
local function enqueue(entry)
Queue.enqueue(
queue_conf({ name = "no-batch", max_batch_size = 1 }),
function()
process_count = process_count + 1
return true
end,
nil,
entry
)
end
enqueue("One")
enqueue("Two")
wait_until_queue_done("no-batch")
assert.equals(2, process_count)
end)
it("batches messages when `max_batch_size` is 2", function()
local process_count = 0
local first_entry, last_entry
local function enqueue(entry)
Queue.enqueue(
queue_conf({
name = "batch",
max_batch_size = 2,
max_coalescing_delay = 0.1,
}),
function(_, batch)
first_entry = first_entry or batch[1]
last_entry = batch[#batch]
process_count = process_count + 1
return true
end,
nil,
entry
)
end
enqueue("One")
enqueue("Two")
enqueue("Three")
enqueue("Four")
enqueue("Five")
wait_until_queue_done("batch")
assert.equals(3, process_count)
assert.equals("One", first_entry)
assert.equals("Five", last_entry)
end)
it("batches messages during shutdown", function()
_G.ngx.worker.exiting = function()
return true
end
local process_count = 0
local first_entry, last_entry
local function enqueue(entry)
Queue.enqueue(
queue_conf({
name = "batch",
max_batch_size = 2,
max_coalescing_delay = 0.1,
}),
function(_, batch)
first_entry = first_entry or batch[1]
last_entry = batch[#batch]
process_count = process_count + 1
return true
end,
nil,
entry
)
end
enqueue("One")
enqueue("Two")
enqueue("Three")
enqueue("Four")
enqueue("Five")
wait_until_queue_done("batch")
assert.equals(3, process_count)
assert.equals("One", first_entry)
assert.equals("Five", last_entry)
end)
it("observes the `max_coalescing_delay` parameter", function()
local process_count = 0
local first_entry, last_entry
local function enqueue(entry)
Queue.enqueue(
queue_conf({
name = "batch",
max_batch_size = 2,
max_coalescing_delay = 3,
}),
function(_, batch)
first_entry = first_entry or batch[1]
last_entry = batch[#batch]
process_count = process_count + 1
return true
end,
nil,
entry
)
end
enqueue("One")
ngx.sleep(1)
enqueue("Two")
wait_until_queue_done("batch")
assert.equals(1, process_count)
assert.equals("One", first_entry)
assert.equals("Two", last_entry)
end)
it("retries sending messages", function()
local process_count = 0
local entry
Queue.enqueue(
queue_conf({
name = "retry",
max_batch_size = 1,
max_coalescing_delay = 0.1,
}),
function(_, batch)
entry = batch[1]
process_count = process_count + 1
return process_count == 2
end,
nil,
"Hello"
)
wait_until_queue_done("retry")
assert.equal(2, process_count)
assert.equal("Hello", entry)
end)
it("gives up sending after retrying", function()
local function enqueue(entry)
Queue.enqueue(
queue_conf({
name = "retry-give-up",
max_batch_size = 1,
max_retry_time = 1,
max_coalescing_delay = 0.1,
}),
function()
return false, "FAIL FAIL FAIL"
end,
nil,
entry
)
end
enqueue("Hello")
enqueue("another value")
wait_until_queue_done("retry-give-up")
assert.match_re(log_messages, 'WARN .* handler could not process entries: FAIL FAIL FAIL')
assert.match_re(log_messages, 'ERR .*1 queue entries were lost')
end)
it("warns when queue reaches its capacity limit", function()
local capacity = 100
local function enqueue(entry)
Queue.enqueue(
queue_conf({
name = "capacity-warning",
max_batch_size = 1,
max_entries = capacity,
max_coalescing_delay = 0.1,
}),
function()
return false
end,
nil,
entry
)
end
for _ = 1, math.floor(capacity * Queue._CAPACITY_WARNING_THRESHOLD) - 1 do
enqueue("something")
end
assert.has.no.match_re(log_messages, "WARN .*queue at \\d*% capacity")
enqueue("something")
enqueue("something")
assert.match_re(log_messages, "WARN .*queue at \\d*% capacity")
log_messages = ""
enqueue("something")
assert.has.no.match_re(
log_messages,
"WARN .*queue at \\d*% capacity",
"the capacity warning should not be logged more than once"
)
end)
it("drops entries when queue reaches its capacity", function()
local processed
local function enqueue(entry)
Queue.enqueue(
queue_conf({
name = "capacity-exceeded",
max_batch_size = 2,
max_entries = 2,
max_coalescing_delay = 0.1,
}),
function(_, batch)
processed = batch
return true
end,
nil,
entry
)
end
enqueue("One")
enqueue("Two")
enqueue("Three")
enqueue("Four")
enqueue("Five")
wait_until_queue_done("capacity-exceeded")
assert.equal("Four", processed[1])
assert.equal("Five", processed[2])
assert.match_re(log_messages, "ERR .*queue full")
enqueue("Six")
wait_until_queue_done("capacity-exceeded")
assert.equal("Six", processed[1])
assert.match_re(log_messages, "INFO .*queue resumed processing")
end)
it("queue does not fail for max batch size = max entries", function()
local fail_process = true
local function enqueue(entry)
Queue.enqueue(
queue_conf({
name = "capacity-exceeded",
max_batch_size = 2,
max_entries = 2,
max_coalescing_delay = 0.1,
}),
function(_, batch)
ngx.sleep(1)
if fail_process then
return false, "FAIL FAIL FAIL"
end
return true
end,
nil,
entry
)
end
-- enqueue 2 entries, enough for first batch
for i = 1, 2 do
enqueue("initial batch: " .. tostring(i))
end
-- wait for max_coalescing_delay such that the first batch is processed (and will be stuck in retry loop, as our handler always fails)
ngx.sleep(0.1)
-- fill in some more entries
for i = 1, 2 do
enqueue("fill up: " .. tostring(i))
end
fail_process = false
wait_until_queue_done("capacity-exceeded")
end)
it("drops entries when it reaches its max_bytes", function()
local processed
local function enqueue(entry)
Queue.enqueue(
queue_conf({
name = "string-capacity-exceeded",
max_batch_size = 1,
max_bytes = 6,
max_retry_time = 1,
}),
function(_, entries)
processed = entries
return true
end,
nil,
entry
)
end
enqueue("1")
enqueue("22")
enqueue("333")
enqueue("4444")
wait_until_queue_done("string-capacity-exceeded")
assert.equal("4444", processed[1])
assert.match_re(log_messages, "ERR .*byte capacity exceeded, 3 queue entries were dropped")
enqueue("55555")
wait_until_queue_done("string-capacity-exceeded")
assert.equal("55555", processed[1])
enqueue("666666")
wait_until_queue_done("string-capacity-exceeded")
assert.equal("666666", processed[1])
end)
it("warns about improper max_bytes setting", function()
local function enqueue(entry)
Queue.enqueue(
queue_conf({
name = "string-capacity-warnings",
max_batch_size = 1,
max_bytes = 1,
}),
function ()
return true
end,
nil,
entry
)
end
enqueue("23")
assert.match_re(log_messages,
[[ERR .*string to be queued is longer \(2 bytes\) than the queue's max_bytes \(1 bytes\)]])
log_messages = ""
enqueue({ foo = "bar" })
assert.match_re(log_messages,
"ERR .*queuing non-string entry to a queue that has queue.max_bytes set, capacity monitoring will not be correct")
end)
it("queue is deleted when it is done sending", function()
local process_count = 0
local function enqueue(entry)
Queue.enqueue(
queue_conf({ name = "no-garbage" }),
function()
process_count = process_count + 1
return true
end,
nil,
entry
)
end
enqueue("Hello World")
assert.is_truthy(Queue.exists("no-garbage"))
helpers.wait_until(
function()
return not Queue.exists("no-garbage")
end,
10)
enqueue("and some more")
helpers.wait_until(
function()
return process_count == 2
end,
10)
end)
it("sends data quickly", function()
local entry_count = 1000
local last
for i = 1,entry_count do
Queue.enqueue(
queue_conf({
name = "speedy-sending",
max_batch_size = 10,
max_coalescing_delay = 0.1,
}),
function(_, entries)
last = entries[#entries]
return true
end,
nil,
i
)
end
helpers.wait_until(
function ()
return last == entry_count
end,
1
)
end)
it("works when time is moved forward while items are being queued", function()
local number_of_entries = 1000
local number_of_entries_processed = 0
now_offset = 1
for i = 1, number_of_entries do
Queue.enqueue(
queue_conf({
name = "time-forwards-adjust",
max_batch_size = 10,
max_coalescing_delay = 10,
}),
function(_, entries)
number_of_entries_processed = number_of_entries_processed + #entries
return true
end,
nil,
i
)
-- manipulate the current time forwards to simulate multiple time changes while entries are added to the queue
now_offset = now_offset + now_offset
end
helpers.wait_until(
function ()
return number_of_entries_processed == number_of_entries
end,
10
)
end)
it("works when time is moved backward while items are being queued", function()
-- In this test, we move the time forward while we're sending out items. The parameters are chosen so that
-- time changes are likely to occur while enqueuing and while sending.
local number_of_entries = 100
local number_of_entries_processed = 0
now_offset = -1
for i = 1, number_of_entries do
Queue.enqueue(
queue_conf({
name = "time-backwards-adjust",
max_batch_size = 10,
max_coalescing_delay = 10,
}),
function(_, entries)
number_of_entries_processed = number_of_entries_processed + #entries
ngx.sleep(0.2)
return true
end,
nil,
i
)
ngx.sleep(0.01)
now_offset = now_offset + now_offset
end
helpers.wait_until(
function ()
return number_of_entries_processed == number_of_entries
end,
10
)
end)
it("works when time is moved backward while items are on the queue and not yet processed", function()
-- In this test, we manipulate the time backwards while we're sending out items. The parameters are chosen so that
-- time changes are likely to occur while enqueuing and while sending.
local number_of_entries = 100
local last
local qconf = queue_conf({
name = "time-backwards-blocked-adjust",
max_batch_size = 10,
max_coalescing_delay = 0.1,
})
local handler = function(_, entries)
last = entries[#entries]
ngx.sleep(0.2)
return true
end
now_offset = -1
for i = 1, number_of_entries do
Queue.enqueue(qconf, handler, nil, i)
ngx.sleep(0.01)
now_offset = now_offset - 1000
end
helpers.wait_until(
function()
return not Queue._exists(qconf.name)
end,
10
)
Queue.enqueue(qconf, handler, nil, "last")
helpers.wait_until(
function()
return last == "last"
end,
10
)
end)
it("works when time is moved forward while items are on the queue and not yet processed", function()
local number_of_entries = 100
local last
local qconf = queue_conf({
name = "time-forwards-blocked-adjust",
max_batch_size = 10,
max_coalescing_delay = 0.1,
})
local handler = function(_, entries)
last = entries[#entries]
ngx.sleep(0.2)
return true
end
now_offset = 1
for i = 1, number_of_entries do
Queue.enqueue(qconf, handler, nil, i)
now_offset = now_offset + 1000
end
helpers.wait_until(
function()
return not Queue._exists(qconf.name)
end,
10
)
Queue.enqueue(qconf, handler, nil, "last")
helpers.wait_until(
function()
return last == "last"
end,
10
)
end)
it("converts common legacy queue parameters", function()
local legacy_parameters = {
retry_count = 123,
queue_size = 234,
flush_timeout = 345,
queue = {
name = "common-legacy-conversion-test",
},
}
local converted_parameters = Queue.get_plugin_params("someplugin", legacy_parameters)
assert.match_re(log_messages, 'the retry_count parameter no longer works, please update your configuration to use initial_retry_delay and max_retry_time instead')
assert.equals(legacy_parameters.queue_size, converted_parameters.max_batch_size)
assert.match_re(log_messages, 'the queue_size parameter is deprecated, please update your configuration to use queue.max_batch_size instead')
assert.equals(legacy_parameters.flush_timeout, converted_parameters.max_coalescing_delay)
assert.match_re(log_messages, 'the flush_timeout parameter is deprecated, please update your configuration to use queue.max_coalescing_delay instead')
end)
it("converts opentelemetry plugin legacy queue parameters", function()
local legacy_parameters = {
batch_span_count = 234,
batch_flush_delay = 345,
queue = {
name = "opentelemetry-legacy-conversion-test",
},
}
local converted_parameters = Queue.get_plugin_params("someplugin", legacy_parameters)
assert.equals(legacy_parameters.batch_span_count, converted_parameters.max_batch_size)
assert.match_re(log_messages, 'the batch_span_count parameter is deprecated, please update your configuration to use queue.max_batch_size instead')
assert.equals(legacy_parameters.batch_flush_delay, converted_parameters.max_coalescing_delay)
assert.match_re(log_messages, 'the batch_flush_delay parameter is deprecated, please update your configuration to use queue.max_coalescing_delay instead')
end)
it("logs deprecation messages only every so often", function()
local legacy_parameters = {
retry_count = 123,
queue = {
name = "legacy-warning-suppression",
},
}
for _ = 1,10 do
Queue.get_plugin_params("someplugin", legacy_parameters)
end
assert.equals(1, count_matching_log_messages('the retry_count parameter no longer works'))
now_offset = 1000
for _ = 1,10 do
Queue.get_plugin_params("someplugin", legacy_parameters)
end
assert.equals(2, count_matching_log_messages('the retry_count parameter no longer works'))
end)
it("defaulted legacy parameters are ignored when converting", function()
local legacy_parameters = {
queue_size = 1,
flush_timeout = 2,
batch_span_count = 200,
batch_flush_delay = 3,
queue = {
max_batch_size = 123,
max_coalescing_delay = 234,
}
}
local converted_parameters = Queue.get_plugin_params("someplugin", legacy_parameters)
assert.equals(123, converted_parameters.max_batch_size)
assert.equals(234, converted_parameters.max_coalescing_delay)
end)
it("continue processing after hard error in handler", function()
local processed = {}
local function enqueue(entry)
Queue.enqueue(
queue_conf({
name = "continue-processing",
max_batch_size = 1,
max_entries = 5,
max_coalescing_delay = 0.1,
max_retry_time = 3,
}),
function(_, batch)
if batch[1] == "Two" then
error("hard error")
end
table.insert(processed, batch[1])
return true
end,
nil,
entry
)
end
enqueue("One")
enqueue("Two")
enqueue("Three")
wait_until_queue_done("continue-processing")
assert.equal("One", processed[1])
assert.equal("Three", processed[2])
assert.match_re(log_messages, 'WARN \\[\\] queue continue-processing: handler could not process entries: .*: hard error')
assert.match_re(log_messages, 'ERR \\[\\] queue continue-processing: could not send entries due to max_retry_time exceeded. \\d queue entries were lost')
end)
it("sanity check for function Queue.is_full() & Queue.can_enqueue()", function()
local queue_conf = {
name = "queue-full-checking-too-many-entries",
max_batch_size = 99999, -- avoiding automatically flushing,
max_entries = 2,
max_bytes = nil, -- avoiding bytes limit
max_coalescing_delay = 99999, -- avoiding automatically flushing,
max_retry_time = 60,
initial_retry_delay = 1,
max_retry_delay = 60,
concurrency_limit = 1,
}
local function enqueue(queue_conf, entry)
Queue.enqueue(
queue_conf,
function()
return true
end,
nil,
entry
)
end
-- should be true if the queue does not exist
assert.is_true(Queue.can_enqueue(queue_conf))
assert.is_false(Queue.is_full(queue_conf))
assert.is_true(Queue.can_enqueue(queue_conf, "One"))
enqueue(queue_conf, "One")
assert.is_false(Queue.is_full(queue_conf))
assert.is_true(Queue.can_enqueue(queue_conf, "Two"))
enqueue(queue_conf, "Two")
assert.is_true(Queue.is_full(queue_conf))
assert.is_false(Queue.can_enqueue(queue_conf, "Three"))
queue_conf = {
name = "queue-full-checking-too-many-bytes",
max_batch_size = 99999, -- avoiding automatically flushing,
max_entries = 99999, -- big enough to avoid entries limit
max_bytes = 2,
max_coalescing_delay = 99999, -- avoiding automatically flushing,
max_retry_time = 60,
initial_retry_delay = 1,
max_retry_delay = 60,
concurrency_limit = 1,
}
-- should be true if the queue does not exist
assert.is_true(Queue.can_enqueue(queue_conf))
assert.is_false(Queue.is_full(queue_conf))
assert.is_true(Queue.can_enqueue(queue_conf, "1"))
enqueue(queue_conf, "1")
assert.is_false(Queue.is_full(queue_conf))
assert.is_true(Queue.can_enqueue(queue_conf, "2"))
enqueue(queue_conf, "2")
assert.is_true(Queue.is_full(queue_conf))
assert.is_false(Queue.can_enqueue(queue_conf, "3"))
queue_conf = {
name = "queue-full-checking-too-large-entry",
max_batch_size = 99999, -- avoiding automatically flushing,
max_entries = 99999, -- big enough to avoid entries limit
max_bytes = 3,
max_coalescing_delay = 99999, -- avoiding automatically flushing,
max_retry_time = 60,
initial_retry_delay = 1,
max_retry_delay = 60,
concurrency_limit = 1,
}
-- should be true if the queue does not exist
assert.is_true(Queue.can_enqueue(queue_conf))
enqueue(queue_conf, "1")
assert.is_false(Queue.is_full(queue_conf))
assert.is_true(Queue.can_enqueue(queue_conf, "1"))
assert.is_true(Queue.can_enqueue(queue_conf, "11"))
assert.is_false(Queue.can_enqueue(queue_conf, "111"))
end)
end)