-- 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!")