209 lines
6.7 KiB
Lua
209 lines
6.7 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 Lock = copas.lock
|
|
local gettime = copas.gettime
|
|
|
|
local test_complete = false
|
|
copas.loop(function()
|
|
|
|
local lock1 = Lock.new(nil, true) -- not re-entrant
|
|
assert(lock1:get())
|
|
local s = gettime()
|
|
local _, err = lock1:get(1)
|
|
local duration = gettime() - s
|
|
assert(err == "timeout", "got errror: "..tostring(err))
|
|
assert(duration > 1 and duration < 1.2, string.format("expected timeout of 1 second, but took: %f",duration))
|
|
|
|
-- let go and reacquire
|
|
assert(lock1:release())
|
|
local _, err = lock1:release()
|
|
assert(err == "cannot release a lock not owned", "got error: "..tostring(err))
|
|
|
|
assert(lock1:get())
|
|
lock1:destroy()
|
|
local _, err = lock1:release()
|
|
assert(err == "destroyed", "got errror: "..tostring(err))
|
|
|
|
|
|
-- let's scale, go grab a lock
|
|
lock1 = assert(Lock.new(10))
|
|
assert(lock1:get())
|
|
|
|
local success_count = 0
|
|
local timeout_count = 0
|
|
local destroyed_count = 0
|
|
-- now add another bunch of threads for the same lock
|
|
local size = 750 -- must be multiple of 3 !!
|
|
print("creating "..size.." threads hitting the lock...", gettime())
|
|
local tracker = {}
|
|
for i = 1, size do
|
|
tracker[i] = true
|
|
copas.addthread(function()
|
|
local timeout
|
|
if i > (size*2)/3 then
|
|
timeout = 60 -- the ones to hit "destroyed"
|
|
elseif i > size/3 and i <= (size*2)/3 then
|
|
timeout = 2 -- the ones to hit "timeout"
|
|
else
|
|
timeout = 1 -- the ones to succeed
|
|
end
|
|
--print(i, "waiting...")
|
|
local ok, err = lock1:get(timeout)
|
|
if ok then
|
|
--print(i, "got it!")
|
|
success_count = success_count + 1
|
|
if i == size/3 then
|
|
copas.pause(3) -- keep it long enough for the next 500 to timeout
|
|
--print(i, "releasing ")
|
|
assert(lock1:release()) -- by now the 2nd 500 timed out
|
|
--print(i, "destroying ")
|
|
assert(lock1:destroy()) -- make the last 500 fail on "destroyed"
|
|
else
|
|
--print(i, "releasing ")
|
|
assert(lock1:release())
|
|
end
|
|
tracker[i] = nil
|
|
|
|
elseif err == "timeout" then
|
|
--print(i, "timed out!")
|
|
timeout_count = timeout_count + 1
|
|
--if i == (size*2)/3 then
|
|
-- copas.pause(2) -- to ensure thread 500 finished its sleep above
|
|
--end
|
|
tracker[i] = nil
|
|
|
|
elseif err == "destroyed" then
|
|
--print(i, "destroyed!")
|
|
destroyed_count = destroyed_count + 1
|
|
tracker[i] = nil
|
|
|
|
else
|
|
tracker[i] = nil
|
|
error("didn't expect error: '"..tostring(err).."' thread "..i)
|
|
end
|
|
|
|
end) -- added thread function
|
|
end -- for loop
|
|
print("releasing "..size.." threads...", gettime())
|
|
assert(lock1:release())
|
|
print("waiting to finish...")
|
|
while next(tracker) do copas.pause(0.1) end
|
|
-- check results
|
|
print("success: ", success_count)
|
|
print("timeout: ", timeout_count)
|
|
print("destroyed: ", destroyed_count)
|
|
assert(success_count == size/3)
|
|
assert(timeout_count == size/3)
|
|
assert(destroyed_count == size/3)
|
|
|
|
test_complete = true
|
|
end)
|
|
assert(test_complete, "test did not complete!")
|
|
|
|
|
|
-- Test 2: canceling a queued waiter must not permanently transfer ownership
|
|
-- to it, locking out every legitimate waiter behind it forever.
|
|
-- See https://github.com/lunarmodules/copas/issues/199
|
|
local test2_complete = false
|
|
copas.loop(function()
|
|
|
|
local lock2 = assert(Lock.new(5))
|
|
assert(lock2:get()) -- owned by this (the main) coroutine
|
|
|
|
-- queue a waiter that will be canceled (eg. via future:cancel()) while
|
|
-- it is still waiting in line for the lock
|
|
local canceled_co = copas.addthread(function()
|
|
lock2:get()
|
|
end)
|
|
|
|
-- queue a legitimate waiter behind it
|
|
local waiter_result
|
|
copas.addthread(function()
|
|
local ok, err = lock2:get()
|
|
waiter_result = ok and "got it" or err
|
|
if ok then
|
|
assert(lock2:release())
|
|
end
|
|
end)
|
|
|
|
copas.pause(0.1) -- let both threads enqueue behind the lock
|
|
|
|
copas.removethread(canceled_co) -- simulate external cancellation
|
|
|
|
assert(lock2:release()) -- should hand the lock to the legitimate waiter
|
|
|
|
copas.pause(0.1)
|
|
|
|
assert(waiter_result == "got it",
|
|
"expected the legitimate waiter to get the lock, got: "..tostring(waiter_result))
|
|
assert(lock2.owner == nil, "expected the lock to be free again")
|
|
|
|
test2_complete = true
|
|
end)
|
|
assert(test2_complete, "test 2 did not complete!")
|
|
|
|
|
|
-- Test 3: a failed zero-timeout acquisition must not remain queued.
|
|
-- See https://github.com/lunarmodules/copas/issues/201
|
|
local test3_complete = false
|
|
copas.loop(function()
|
|
|
|
-- 3a: the probe itself must not leave a stale entry behind
|
|
-- (must come from another coroutine: get(0) on the owning coroutine
|
|
-- itself takes the reentrant path, not the queuing one)
|
|
local lock3 = assert(Lock.new(5))
|
|
assert(lock3:get()) -- owner = this (the main) coroutine
|
|
|
|
local prober_err, prober_wait
|
|
copas.addthread(function()
|
|
local _, e, w = lock3:get(0) -- non-blocking probe while locked
|
|
prober_err, prober_wait = e, w
|
|
end)
|
|
copas.pause(0.1) -- let the prober run its probe
|
|
|
|
assert(prober_err == "timeout", "expected immediate timeout, got: "..tostring(prober_err))
|
|
assert(prober_wait == 0)
|
|
|
|
assert(lock3.q_tip == lock3.q_tail,
|
|
"expected the failed zero-timeout probe to not be queued, q_tip="
|
|
..tostring(lock3.q_tip)..", q_tail="..tostring(lock3.q_tail))
|
|
|
|
-- 3b: even if the prober coroutine happens to go to sleep afterwards for
|
|
-- an unrelated reason, a stale queue entry must not let it hijack the
|
|
-- lock next time it is released
|
|
local lock4 = assert(Lock.new(5))
|
|
assert(lock4:get()) -- owner = this (the main) coroutine
|
|
|
|
local prober_done = false
|
|
copas.addthread(function()
|
|
local _, prober_err = lock4:get(0) -- non-blocking probe while locked
|
|
assert(prober_err == "timeout", "expected immediate timeout, got: "..tostring(prober_err))
|
|
copas.pauseforever() -- unrelated sleep, nothing to do with the lock
|
|
prober_done = true
|
|
end)
|
|
copas.pause(0.1) -- let the prober run its probe and go to sleep
|
|
|
|
assert(lock4:release()) -- must not hand the lock to the sleeping prober
|
|
copas.pause(0.1)
|
|
|
|
assert(not prober_done, "the prober should still be asleep, unrelated to the lock")
|
|
assert(lock4.owner == nil, "expected the lock to be free, not hijacked by the prober")
|
|
|
|
local waiter_result
|
|
copas.addthread(function()
|
|
local ok, werr = lock4:get(1)
|
|
waiter_result = ok and "got it" or werr
|
|
end)
|
|
copas.pause(1.2)
|
|
assert(waiter_result == "got it",
|
|
"expected a legitimate waiter to get the freed lock, got: "..tostring(waiter_result))
|
|
|
|
test3_complete = true
|
|
end)
|
|
assert(test3_complete, "test 3 did not complete!")
|
|
|
|
print("test success!")
|