singe/thirdparty/copas/tests/semaphore.lua

394 lines
13 KiB
Lua

-- make sure we are pointing to the local copas first
package.path = string.format("../src/?.lua;%s", package.path)
local copas = require "copas"
local now = copas.gettime
local semaphore = copas.semaphore
local test_complete = false
copas.loop(function()
local sema = semaphore.new(10, 5, 1)
assert(sema:get_count() == 5)
assert(sema:take(3))
assert(sema:get_count() == 2)
local ok, _, err
local start = now()
_, err = sema:take(3, 0) -- 1 too many, immediate timeout
assert(err == "timeout", "expected a timeout")
assert(now() - start < 0.001, "expected it not to block with timeout = 0")
start = now()
_, err = sema:take(10, 0) -- way too many, immediate timeout
assert(err == "timeout", "expected a timeout")
assert(now() - start < 0.001, "expected it not to block with timeout = 0")
start = now()
_, err = sema:take(11) -- more than 'max'; "too many" error
assert(err == "too many", "expected a 'too many' error")
assert(now() - start < 0.001, "expected it not to block")
start = now()
_, err = sema:take(10) -- not too many, let's timeout
assert(err == "timeout", "expected a 'timeout' error")
assert(now() - start > 1, "expected it to block for 1s")
assert(sema:get_count() == 2)
--validate async threads
local state = 0
copas.addthread(function()
assert(sema:take(5))
print("got the first 5!")
state = state + 1
end)
copas.addthread(function()
assert(sema:take(5))
print("got the next 5!")
state = state + 2
end)
copas.pause(0.1)
assert(state == 0, "expected state to still be 0")
assert(sema:get_count() == 2, "expected count to still have 2 resources")
assert(sema:give(4))
assert(sema:get_count() == 1, "expected count to now have 1 resource")
copas.pause(0.1)
assert(state == 1, "expected 1 from the first thread to be added to state")
assert(sema:give(4))
assert(sema:get_count() == 0, "gave 4 more, so 5 in total, releasing 5, leaves 0 as expected")
copas.pause(0.1)
assert(state == 3, "expected 2 from the 2nd thread to be added to state")
ok, err = sema:give(100)
assert(not ok)
assert(err == "too many")
assert(sema:get_count() == 10)
-- validate destroying
assert(sema:take(sema:get_count())) -- empty the semaphore
assert(sema:get_count() == 0, "should be empty now")
local state = 0
copas.addthread(function()
local ok, err = sema:take(5)
if ok then
print("got 5, this is unexpected")
elseif err == "destroyed" then
state = state + 1
end
end)
copas.addthread(function()
local ok, err = sema:take(5)
if ok then
print("got 5, this is unexpected")
elseif err == "destroyed" then
state = state + 1
end
end)
copas.pause(0.1)
assert(sema:destroy())
copas.pause(0.1)
assert(state == 2, "expected 2 threads to error with 'destroyed'")
-- only returns errors from now on, on all methods
ok, err = sema:destroy(); assert(ok == nil and err == "destroyed", "expected an error")
ok, err = sema:give(1); assert(ok == nil and err == "destroyed", "expected an error")
ok, err = sema:take(1); assert(ok == nil and err == "destroyed", "expected an error")
ok, err = sema:get_count(); assert(ok == nil and err == "destroyed", "expected an error")
-- timeouts get cancelled upon destruction
-- we set a timeout to 0.5 seconds, then destroy the semaphore
-- the timeout should not execute
-- Reproduce https://github.com/lunarmodules/copas/issues/118
local track_table = setmetatable({}, { __mode = "v" })
local sema = semaphore.new(10, 0, 0.5)
track_table.sema = sema
local state = 0
track_table.coro = copas.addthread(function()
local ok, err = sema:take(1)
if ok then
print("got one, this is unexpected")
elseif err == "destroyed" then
state = state + 1
end
end)
copas.pause(0.1)
assert(sema:destroy())
copas.pause(0.1)
assert(state == 1, "expected 1 thread to error with 'destroyed'")
sema = nil
local errors = 0
copas.setErrorHandler(function(msg)
print("got error: "..tostring(msg))
print("--------------------------------------")
errors = errors + 1
end, true)
collectgarbage() -- collect garbage to force eviction from the semaphore registry
collectgarbage()
copas.pause(0.5) -- wait for the timeout to expire if it is still set
assert(errors == 0, "expected no errors")
test_complete = true
end)
assert(test_complete, "test did not complete!")
-- Test 2: canceling a queued waiter must not permanently leak the
-- resources handed to it, nor starve legitimate waiters behind it.
-- See https://github.com/lunarmodules/copas/issues/199 (same root cause
-- as the copas.lock bug: `copas.wakeup()` failures were ignored).
local test2_complete = false
copas.loop(function()
-- 2a: a lone canceled waiter must not leak the resources given to it
local sema2 = semaphore.new(10, 0, 5)
local canceled_co = copas.addthread(function()
sema2:take(5)
end)
copas.pause(0.1) -- let it enqueue
copas.removethread(canceled_co) -- simulate external cancellation
assert(sema2:give(5))
copas.pause(0.1)
assert(sema2:get_count() == 5,
"expected the 5 given resources to remain available, got: "..tostring(sema2:get_count()))
-- 2b: a legitimate waiter behind a canceled one must still be served
local sema3 = semaphore.new(10, 0, 5)
local canceled_co2 = copas.addthread(function()
sema3:take(5)
end)
local waiter_result
copas.addthread(function()
local ok, err = sema3:take(5)
waiter_result = ok and "got it" or err
end)
copas.pause(0.1) -- let both enqueue
copas.removethread(canceled_co2) -- simulate external cancellation
assert(sema3:give(5))
copas.pause(0.1)
assert(waiter_result == "got it",
"expected the legitimate waiter to be served, got: "..tostring(waiter_result))
-- 2c: a canceled waiter requesting MORE than what's available must not
-- block a legitimate waiter behind it that requests less; the canceled
-- entry has to be dropped regardless of resource sufficiency, not just
-- when there happen to be enough resources to satisfy its own request.
local sema4 = semaphore.new(10, 0, 5)
local canceled_co3 = copas.addthread(function()
sema4:take(2) -- will be canceled while waiting for 2
end)
local waiter_result2
copas.addthread(function()
local ok, err = sema4:take(1) -- only needs 1
waiter_result2 = ok and "got it" or err
end)
copas.pause(0.1) -- let both enqueue
copas.removethread(canceled_co3) -- simulate external cancellation
assert(sema4:give(1)) -- not enough for the canceled request (2), plenty for the real one (1)
copas.pause(0.1)
assert(waiter_result2 == "got it",
"expected the smaller legitimate waiter to be served ahead of a stale bigger request, got: "
..tostring(waiter_result2))
assert(sema4:get_count() == 0,
"expected the 1 given resource to have gone to the waiter, got: "..tostring(sema4:get_count()))
test2_complete = true
end)
assert(test2_complete, "test 2 did not complete!")
-- Test 3: get_wait() must not count queued waiters that were canceled
-- externally (eg. via copas.removethread) -- they will never consume the
-- resources handed to them, so counting their `requested` overstates how
-- much is actually needed to release everyone still legitimately waiting.
local test3_complete = false
copas.loop(function()
local sema = semaphore.new(10, 0, 5)
local canceled_co = copas.addthread(function() sema:take(1) end)
copas.addthread(function() sema:take(1) end)
copas.addthread(function() sema:take(1) end)
copas.pause(0.1) -- let all 3 enqueue
assert(sema:get_wait() == 3, "expected all 3 live waiters to be counted")
copas.removethread(canceled_co) -- simulate external cancellation
assert(sema:get_wait() == 2,
"expected the canceled waiter to be excluded, got: "..tostring(sema:get_wait()))
test3_complete = true
end)
assert(test3_complete, "test 3 did not complete!")
-- Test 4: release_all() must release every waiter regardless of how many
-- times `max` it takes to do so (unlike give(math.huge), which is capped
-- at `max` and strands the rest). See https://github.com/lunarmodules/copas/issues/203.
local test4_complete = false
copas.loop(function()
local sema = semaphore.new(2, 0, 5) -- max = 2, well below the waiter count
local released = 0
for _ = 1, 5 do
copas.addthread(function()
assert(sema:take(1))
released = released + 1
end)
end
copas.pause(0.1) -- let all 5 enqueue
sema:release_all()
copas.pause(0.1)
assert(released == 5, "expected all 5 waiters to be released, got: "..tostring(released))
assert(sema:get_count() == 0,
"expected no leftover balance after releasing everyone, got: "..tostring(sema:get_count()))
-- release_all() combined with a canceled waiter mixed into the queue
-- must not leave a stray leftover balance either (that would be the
-- fixed get_wait() bug resurfacing through release_all()).
local sema2 = semaphore.new(2, 0, 5)
local canceled_co = copas.addthread(function() sema2:take(1) end)
local released2 = 0
for _ = 1, 3 do
copas.addthread(function()
assert(sema2:take(1))
released2 = released2 + 1
end)
end
copas.pause(0.1) -- let all 4 enqueue
copas.removethread(canceled_co) -- simulate external cancellation
sema2:release_all()
copas.pause(0.1)
assert(released2 == 3, "expected the 3 legitimate waiters to be released, got: "..tostring(released2))
assert(sema2:get_count() == 0,
"expected no leftover balance, got: "..tostring(sema2:get_count()))
test4_complete = true
end)
assert(test4_complete, "test 4 did not complete!")
-- Test 5: destroy() must release EVERY waiting thread, even when there are
-- more of them than `max`. It used to call give(math.huge), which is
-- capped at `max` and stranded the excess forever -- the same root cause
-- as https://github.com/lunarmodules/copas/issues/203.
local test5_complete = false
copas.loop(function()
local sema = semaphore.new(2, 0, 5) -- max = 2, well below the waiter count
local destroyed_count = 0
for _ = 1, 5 do
copas.addthread(function()
local ok, err = sema:take(1)
if not ok and err == "destroyed" then
destroyed_count = destroyed_count + 1
end
end)
end
copas.pause(0.1) -- let all 5 enqueue
assert(sema:destroy())
copas.pause(0.1)
assert(destroyed_count == 5,
"expected all 5 waiters to be released on destroy, got: "..tostring(destroyed_count))
test5_complete = true
end)
assert(test5_complete, "test 5 did not complete!")
-- Test 6: give() must reject invalid input instead of silently corrupting
-- the resource accounting.
local test6_complete = false
copas.loop(function()
local sema = semaphore.new(10, 0, 5)
-- a negative amount must error, not push `count` negative
local ok = pcall(function() sema:give(-1) end)
assert(not ok, "expected give(-1) to raise an error")
assert(sema:get_count() == 0, "count must be unaffected by the rejected call")
-- math.huge must error too; even on an uncapped semaphore (max = math.huge)
-- it would leave `count` permanently `inf`, since there is no cap left to
-- clamp it back down.
local uncapped = semaphore.new(math.huge, 0, 5)
ok = pcall(function() uncapped:give(math.huge) end)
assert(not ok, "expected give(math.huge) on an uncapped semaphore to raise an error")
assert(uncapped:get_count() == 0, "count must be unaffected by the rejected call")
-- NaN must error too
ok = pcall(function() sema:give(0/0) end)
assert(not ok, "expected give(NaN) to raise an error")
-- 0 remains a valid no-op
assert(sema:give(0))
assert(sema:get_count() == 0)
-- no argument still defaults to 1
assert(sema:give())
assert(sema:get_count() == 1)
test6_complete = true
end)
assert(test6_complete, "test 6 did not complete!")
-- Test 7: take() must reject invalid input instead of silently bypassing
-- `max` and corrupting the resource count.
local test7_complete = false
copas.loop(function()
local sema = semaphore.new(5, 0, 5)
-- a negative request must error, not add to `count` on the fast path
local ok = pcall(function() sema:take(-100) end)
assert(not ok, "expected take(-100) to raise an error")
assert(sema:get_count() == 0, "count must be unaffected by the rejected call, max was 5")
-- 0 must error too; it's not a meaningful amount of resources to request
ok = pcall(function() sema:take(0) end)
assert(not ok, "expected take(0) to raise an error")
-- NaN must error too; it would otherwise queue forever, since
-- `count >= NaN` is always false and can never be satisfied
ok = pcall(function() sema:take(0/0) end)
assert(not ok, "expected take(NaN) to raise an error")
-- no argument still defaults to 1 and works normally
assert(sema:give(1))
assert(sema:take())
assert(sema:get_count() == 0)
test7_complete = true
end)
assert(test7_complete, "test 7 did not complete!")
print("test success!")