From 72d7b36e020fd3f11ec002c110e7340f667d6628 Mon Sep 17 00:00:00 2001 From: Benoit Germain Date: Thu, 3 Jul 2025 18:11:13 +0200 Subject: Fix more issues related to suspended coroutines and join/indexing operations * indexing and joining a suspended lane closes the coroutine, causing the termination of the lane * properly close coroutine to-be-closed variables on interruption --- unit_tests/scripts/coro/collect_yielded_lane.lua | 105 +++++++++++++++++++---- unit_tests/scripts/coro/yielding_function.lua | 34 +++----- 2 files changed, 100 insertions(+), 39 deletions(-) (limited to 'unit_tests/scripts') diff --git a/unit_tests/scripts/coro/collect_yielded_lane.lua b/unit_tests/scripts/coro/collect_yielded_lane.lua index 0459698..2bc4ae8 100644 --- a/unit_tests/scripts/coro/collect_yielded_lane.lua +++ b/unit_tests/scripts/coro/collect_yielded_lane.lua @@ -1,4 +1,5 @@ -local lanes = require "lanes" +local fixture = require "fixture" +local lanes = require "lanes".configure{on_state_create = fixture.on_state_create} local fixture = require "fixture" lanes.finally(fixture.throwing_finalizer) @@ -7,18 +8,27 @@ local utils = lanes.require "_utils" local PRINT = utils.MAKE_PRINT() -- a lane body that yields stuff -local yielder = function(out_linda_) +local yielder = function(out_linda_, wait_) + local fixture = assert(require "fixture") -- here is a to-be-closed variable that, when closed, sends "Closed!" in the "out" slot of the provided linda local t = setmetatable( { text = "Closed!" }, { __close = function(self, err) + if wait_ then + fixture.block_for(wait_) + end out_linda_:send("out", self.text) end } ) - -- yield forever + -- yield forever, but be cancel-friendly + local n = 1 while true do - coroutine.yield("I yield!") + coroutine.yield("I yield!", n) + if cancel_test and cancel_test() then -- cancel_test does not exist when run immediately (not in a Lane) + return "I am cancelled" + end + n = n + 1 end end @@ -27,8 +37,8 @@ local out_linda = lanes.linda() local test_close = function(what_, f_) local c = coroutine.create(f_) for i = 1, 10 do - local t, r = coroutine.resume(c, out_linda) -- returns true + - assert(t == true and r == "I yield!", "got " .. tostring(t) .. " " .. tostring(r)) + local t, r1, r2 = coroutine.resume(c, out_linda) -- returns true + + assert(t == true and r1 == "I yield!" and r2 == i, "got " .. tostring(t) .. " " .. tostring(r1) .. " " .. tostring(r2)) local s = coroutine.status(c) assert(s == "suspended") end @@ -39,28 +49,64 @@ local test_close = function(what_, f_) assert(s == "out" and r == "Closed!", what_ .. " got " .. tostring(s) .. " " .. tostring(r)) end --- first, try the close mechanism outside of a lane -test_close("base", yielder) +--------------------------------------------------------- +-- TEST: first, try the close mechanism outside of a lane +--------------------------------------------------------- +if true then + test_close("base", yielder) +end --- try again with a function obtained through dump/undump --- note this means our yielder implementation can't have upvalues, as they are lost in the process -test_close("dumped", load(string.dump(yielder))) +--------------------------------------------------------------- +-- TEST: try again with a function obtained through dump/undump +--------------------------------------------------------------- +if true then + -- note this means our yielder implementation can't have upvalues, as they are lost in the process + test_close("dumped", load(string.dump(yielder))) +end ------------------------------------------------------------------------------ --- TEST: to-be-closed variables are properly closed when the lane is collected +-- TEST: to-be-closed variables are properly closed whzen the lane is collected ------------------------------------------------------------------------------ -if false then -- NOT IMPLEMENTED YET! - +if true then -- the generator local coro_g = lanes.coro("*", yielder) -- start the lane local h = coro_g(out_linda) - -- join it so that it reaches suspended state - local r, v = h:join(0.5) + -- join the lane. it should be done and give back the values resulting of the first yield point + local r, v1, v2 = h:join() + assert(r == true and v1 == "I yield!" and v2 == 1, "got " .. tostring(r) .. " " .. tostring(v1) .. " " .. tostring(v2)) + assert(h.status == "done", "got " .. h.status) + + -- force collection of the lane + h = nil + collectgarbage() + + -- I want the to-be-closed variable of the coroutine linda to be properly closed + local s, r = out_linda:receive(0, "out") + assert(s == "out" and r == "Closed!", "coro got " .. tostring(s) .. " " .. tostring(r)) -- THIS TEST FAILS +end + +--------------------------------------------------------------------------------------------------- +-- TEST: if a to-be-closed handler takes longer than the join timeout, everything works as expected +--------------------------------------------------------------------------------------------------- +if true then + -- the generator + local coro_g = lanes.coro("*", yielder) + + -- start the lane. The to-be-closed handler will sleep for 1 second + local h = coro_g(out_linda, 1) + + -- first join attempt should timeout + local r, v = h:join(0.6) assert(r == nil and v == "timeout", "got " .. tostring(r) .. " " .. tostring(v)) - assert(h.status == "suspended") + assert(h.status == "running", "got " .. h.status) + + -- join the lane again. it should be done and give back the values resulting of the first yield point + local r, v1, v2 = h:join(0.6) + assert(r == true and v1 == "I yield!" and v2 == 1, "got " .. tostring(r) .. " " .. tostring(v1) .. " " .. tostring(v2)) + assert(h.status == "done", "got " .. h.status) -- force collection of the lane h = nil @@ -68,5 +114,28 @@ if false then -- NOT IMPLEMENTED YET! -- I want the to-be-closed variable of the coroutine linda to be properly closed local s, r = out_linda:receive(0, "out") - assert(s == "out" and r == "Closed!", "coro got " .. tostring(s) .. " " .. tostring(r)) + assert(s == "out" and r == "Closed!", "coro got " .. tostring(s) .. " " .. tostring(r)) -- THIS TEST FAILS +end + +-------------------------------------------------- +-- TEST: cancelling a suspended Lane should end it +-------------------------------------------------- +if true then + -- the generator + local coro_g = lanes.coro("*", yielder) + + -- start the lane + local h = coro_g(out_linda) + repeat until h.status == "suspended" + + -- first cancellation attempt: don't wake the lane + local b, r = h:cancel("soft", 0.5) + -- the lane is still blocked in its suspended state + assert(b == false and r == "timeout" and h.status == "suspended", "got " .. tostring(b) .. " " .. tostring(r) .. " " .. h.status) + + -- cancel the Lane again, this time waking it. it will resume, and yielder()'s will break out of its infinite loop + h:cancel("soft", nil, true) + + -- lane should be done, because it returned cooperatively when detecting a soft cancel + assert(h.status == "done", "got " .. h.status) end diff --git a/unit_tests/scripts/coro/yielding_function.lua b/unit_tests/scripts/coro/yielding_function.lua index 636f094..6518d1f 100644 --- a/unit_tests/scripts/coro/yielding_function.lua +++ b/unit_tests/scripts/coro/yielding_function.lua @@ -23,7 +23,7 @@ end -------------------------------------------------------------------------------------------------- -- TEST: if we start a non-coroutine lane with a yielding function, we should get an error, right? -------------------------------------------------------------------------------------------------- -if false then +if true then local fun_g = lanes.gen("*", { name = 'auto' }, yielder) local h = fun_g("hello", "world", "!") local err, status, stack = h:join() @@ -48,7 +48,7 @@ local coro_g = lanes.coro("*", {name = "auto"}, yielder) ------------------------------------------------------------------------------------------------- -- TEST: we can resume as many times as the lane yields, then read the returned value on indexing ------------------------------------------------------------------------------------------------- -if false then +if true then -- launch coroutine lane local h = coro_g("hello", "world", "!") -- read the yielded values, sending back the expected index @@ -63,7 +63,7 @@ end --------------------------------------------------------------------------------------------- -- TEST: we can resume as many times as the lane yields, then read the returned value on join --------------------------------------------------------------------------------------------- -if false then +if true then -- launch coroutine lane local h = coro_g("hello", "world", "!") -- read the yielded values, sending back the expected index @@ -75,9 +75,9 @@ if false then assert(h.status == "done" and s == true and r == "bye!") end ---------------------------------------------------------------------------------------------------- --- TEST: if we join a yielded lane, we get a timeout, and we can resume as if we didn't try to join ---------------------------------------------------------------------------------------------------- +--------------------------------------------------- +-- TEST: if we join a yielded lane, the lane aborts +--------------------------------------------------- if true then -- launch coroutine lane local h = coro_g("hello", "world", "!") @@ -89,10 +89,10 @@ if true then assert(s == "done" and b == true and r == "world", "got " .. s .. " " .. tostring(b) .. " " .. tostring(r)) end ------------------------------------------------------------------------ --- TEST: if we index yielded lane, we should get the last yielded value ------------------------------------------------------------------------ -if false then +------------------------------------------------------------------------- +-- TEST: if we index a yielded lane, we should get the last yielded value +------------------------------------------------------------------------- +if true then -- launch coroutine lane local h = coro_g("hello", "world", "!") -- read the first yielded value, sending back the expected index @@ -102,15 +102,7 @@ if false then local r2 = h[1] local r3 = h[1] assert(r1 == "world" and r2 == "world" and r3 == "world", "got " .. r1 .. " " .. r2 .. " " .. r3) - assert(h:resume(2) == "world") - - -- THERE IS AN INCONSISTENCY: h:resume pulls the yielded values directly out of the lane's stack - -- but h[n] removes them and stores them in the internal values storage table - -- TODO: so we need to decide: should indexing a yielded lane work like resume()? - assert(h:resume(3) == "!") + -- once the lane was indexed, it is no longer resumable (just like after join) + local b, e = pcall(h.resume, h, 2) + assert(b == false and e == "cannot resume non-suspended coroutine Lane") end - -------------------------------------------------------------------------------------- --- TEST: if we close yielded lane, we can join it and get the last yielded values out -------------------------------------------------------------------------------------- --- TODO: we need to implement lane:close() for that! -- cgit v1.2.3-55-g6feb