diff options
| author | Benoit Germain <benoit.germain@ubisoft.com> | 2026-02-27 10:59:11 +0100 |
|---|---|---|
| committer | Benoit Germain <benoit.germain@ubisoft.com> | 2026-02-27 10:59:11 +0100 |
| commit | 7e4b6e1526c5b2f85079df6a6ecfe08244a7dbcc (patch) | |
| tree | 3efb69b5a2d40fd1c226b72f3d0f99dcc30afc83 | |
| parent | 68355c7dd6bef53d264eca53567df9fb9c8684b6 (diff) | |
| download | lanes-7e4b6e1526c5b2f85079df6a6ecfe08244a7dbcc.tar.gz lanes-7e4b6e1526c5b2f85079df6a6ecfe08244a7dbcc.tar.bz2 lanes-7e4b6e1526c5b2f85079df6a6ecfe08244a7dbcc.zip | |
Unify sleep() timeout with send() and receive (nil means forever)v4.0.0
| -rw-r--r-- | CHANGES | 2 | ||||
| -rw-r--r-- | docs/index.html | 20 | ||||
| -rw-r--r-- | src/lanes.cpp | 24 | ||||
| -rw-r--r-- | tests/track_lanes.lua | 2 | ||||
| -rw-r--r-- | unit_tests/lane_tests.cpp | 8 |
5 files changed, 28 insertions, 28 deletions
| @@ -12,7 +12,7 @@ CHANGE 2: BGe 26-Feb-26 | |||
| 12 | - Lanes module: | 12 | - Lanes module: |
| 13 | - shared library is now lanes_core.[so|dll] instead of lanes/core.[so|dll] | 13 | - shared library is now lanes_core.[so|dll] instead of lanes/core.[so|dll] |
| 14 | - lanes.register() is also available as lanes_register() in the exported C API. | 14 | - lanes.register() is also available as lanes_register() in the exported C API. |
| 15 | - lanes.sleep() accepts a new argument "indefinitely" to block forever (until hard cancellation is received). | 15 | - lanes.sleep(): passing nil or no argument now blocks indefinitely (until cancellation is received). |
| 16 | - function set_debug_threadname() available inside a Lane is renamed lane_threadname(); can now both read and write the name. | 16 | - function set_debug_threadname() available inside a Lane is renamed lane_threadname(); can now both read and write the name. |
| 17 | - function cancel_test() raises cancel_error by default in a hard-cancelled lane. | 17 | - function cancel_test() raises cancel_error by default in a hard-cancelled lane. |
| 18 | - new function lanes.finally(). Installs a function that gets called at Lanes shutdown after attempting to terminate all lanes. | 18 | - new function lanes.finally(). Installs a function that gets called at Lanes shutdown after attempting to terminate all lanes. |
diff --git a/docs/index.html b/docs/index.html index c9a85ac..a36119e 100644 --- a/docs/index.html +++ b/docs/index.html | |||
| @@ -218,7 +218,7 @@ | |||
| 218 | <li> | 218 | <li> |
| 219 | Inside the lane | 219 | Inside the lane |
| 220 | <ul> | 220 | <ul> |
| 221 | <li><tt>cancel_test()</tt>: check for cancellation requests</li> | 221 | <li><tt>cancel_test()</tt>: check for <a href="#cancelling">cancellation</a> requests</li> |
| 222 | <li><tt>lane_threadname()</tt>: read or change the name of the thread</li> | 222 | <li><tt>lane_threadname()</tt>: read or change the name of the thread</li> |
| 223 | <li><tt>set_finalizer()</tt>: install a function called when the lane exits</li> | 223 | <li><tt>set_finalizer()</tt>: install a function called when the lane exits</li> |
| 224 | </ul> | 224 | </ul> |
| @@ -226,7 +226,7 @@ | |||
| 226 | <li> | 226 | <li> |
| 227 | Given some <a href="#lindas">linda</a> <tt>l</tt> | 227 | Given some <a href="#lindas">linda</a> <tt>l</tt> |
| 228 | <ul> | 228 | <ul> |
| 229 | <li><tt>l:cancel()</tt>: mark a <a href="#lindas">linda</a> for cancellation</li> | 229 | <li><tt>l:cancel()</tt>: mark a <a href="#lindas">linda</a> for <a href="#cancelling">cancellation</a></li> |
| 230 | <li><tt>l:collectgarbage()</tt>: trigger a GC cycle in the <a href="#lindas">linda</a>'s Keeper state</li> | 230 | <li><tt>l:collectgarbage()</tt>: trigger a GC cycle in the <a href="#lindas">linda</a>'s Keeper state</li> |
| 231 | <li><tt>l:deep()</tt>: obtain a light userdata uniquely representing the <a href="#lindas">linda</a></li> | 231 | <li><tt>l:deep()</tt>: obtain a light userdata uniquely representing the <a href="#lindas">linda</a></li> |
| 232 | <li><tt>l:dump()</tt>: have information about slot contents</li> | 232 | <li><tt>l:dump()</tt>: have information about slot contents</li> |
| @@ -452,8 +452,8 @@ | |||
| 452 | Sets the default period in seconds a <a href="#lindas">linda</a> will wake by itself during blocked operations. Default is never.<br /> | 452 | Sets the default period in seconds a <a href="#lindas">linda</a> will wake by itself during blocked operations. Default is never.<br /> |
| 453 | When a <a href="#lindas">linda</a> enters a blocking call (<tt>send()</tt>, <tt>receive()</tt>, <tt>receive_batched()</tt>, <tt>sleep()</tt>), it normally sleeps either until the operation completes | 453 | When a <a href="#lindas">linda</a> enters a blocking call (<tt>send()</tt>, <tt>receive()</tt>, <tt>receive_batched()</tt>, <tt>sleep()</tt>), it normally sleeps either until the operation completes |
| 454 | or the specified timeout expires. With this setting, the default behavior can be changed to wake periodically. This can help for example with timing issues where a lane is signalled | 454 | or the specified timeout expires. With this setting, the default behavior can be changed to wake periodically. This can help for example with timing issues where a lane is signalled |
| 455 | for cancellation, but a <a href="#lindas">linda</a> inside the lane was in the middle of processing an operation but did not actually start the wait. This can result in the signal to be ignored, thus | 455 | for <a href="#cancelling">cancellation</a>, but a <a href="#lindas">linda</a> inside the lane was in the middle of processing an operation but did not actually start the wait. This can result in the signal to be ignored, thus |
| 456 | causing the <a href="#lindas">linda</a> to wait out the full operation timeout before cancellation is processed. | 456 | causing the <a href="#lindas">linda</a> to wait out the full operation timeout before <a href="#cancelling">cancellation</a> is processed. |
| 457 | </td> | 457 | </td> |
| 458 | </tr> | 458 | </tr> |
| 459 | 459 | ||
| @@ -499,7 +499,7 @@ | |||
| 499 | </td> | 499 | </td> |
| 500 | <td> | 500 | <td> |
| 501 | Sets the duration in seconds Lanes will wait for graceful termination of running lanes at application shutdown. Default is <tt>0.25</tt>.<br /> | 501 | Sets the duration in seconds Lanes will wait for graceful termination of running lanes at application shutdown. Default is <tt>0.25</tt>.<br /> |
| 502 | Lanes signals all lanes for cancellation with <tt>"soft"</tt>, <tt>"hard"</tt>, and <tt>"all"</tt> modes, in that order. Each attempt has <tt>shutdown_timeout</tt> seconds to succeed before the next one.<br /> | 502 | Lanes signals all lanes for <a href="#cancelling">cancellation</a> with <tt>"soft"</tt>, <tt>"hard"</tt>, and <tt>"all"</tt> modes, in that order. Each attempt has <tt>shutdown_timeout</tt> seconds to succeed before the next one.<br /> |
| 503 | Then there is a last chance at cleanup with <a href="#finally"><tt>lanes.finally()</tt></a>. If some lanes are still running after that point, shutdown will either freeze or throw. It is YOUR responsibility to cleanup properly after yourself. | 503 | Then there is a last chance at cleanup with <a href="#finally"><tt>lanes.finally()</tt></a>. If some lanes are still running after that point, shutdown will either freeze or throw. It is YOUR responsibility to cleanup properly after yourself. |
| 504 | IMPORTANT: If there are still running lanes at shutdown, an error is raised, which will be propagated by Lua to the handler installed by <tt>lua_setwarnf</tt>. If the finalizer returned a value, this will be used as the error message.<br /> | 504 | IMPORTANT: If there are still running lanes at shutdown, an error is raised, which will be propagated by Lua to the handler installed by <tt>lua_setwarnf</tt>. If the finalizer returned a value, this will be used as the error message.<br /> |
| 505 | LANES SHUTDOWN WILL NOT BE COMPLETE IN THAT CASE, AND THE SUBSEQUENT CONSEQUENCES ARE UNDEFINED! | 505 | LANES SHUTDOWN WILL NOT BE COMPLETE IN THAT CASE, AND THE SUBSEQUENT CONSEQUENCES ARE UNDEFINED! |
| @@ -1561,7 +1561,7 @@ | |||
| 1561 | <p> | 1561 | <p> |
| 1562 | <tt>get()</tt> can read several values at once, and does not block. Return values ares: | 1562 | <tt>get()</tt> can read several values at once, and does not block. Return values ares: |
| 1563 | <ul> | 1563 | <ul> |
| 1564 | <li><tt>nil, lanes.cancel_error</tt> in case of cancellation.</li> | 1564 | <li><tt>nil, lanes.cancel_error</tt> in case of <a href="#cancelling">cancellation</a>.</li> |
| 1565 | <li><tt>number, val...</tt> where number is the actual count of items obtained from the linda (can be 0).</li> | 1565 | <li><tt>number, val...</tt> where number is the actual count of items obtained from the linda (can be 0).</li> |
| 1566 | </ul> | 1566 | </ul> |
| 1567 | </p> | 1567 | </p> |
| @@ -1768,12 +1768,12 @@ | |||
| 1768 | </p> | 1768 | </p> |
| 1769 | 1769 | ||
| 1770 | <table border="1" bgcolor="#E0E0FF" cellpadding="10" style="width:50%"><tr><td><pre> | 1770 | <table border="1" bgcolor="#E0E0FF" cellpadding="10" style="width:50%"><tr><td><pre> |
| 1771 | nil, "timeout" = lanes.sleep(['indefinitely'|seconds|nil]) | 1771 | nil, "timeout" = lanes.sleep([seconds|nil]) |
| 1772 | </pre></td></tr></table> | 1772 | </pre></td></tr></table> |
| 1773 | 1773 | ||
| 1774 | <p> | 1774 | <p> |
| 1775 | A very simple way of sleeping when nothing else is available. Is implemented by attempting to read some data in an unused channel of the internal <a href="#lindas">linda</a> used for timers (this <a href="#lindas">linda</a> exists even when timers aren't enabled). | 1775 | A very simple way of sleeping when nothing else is available. Is implemented by attempting to read some data in an unused channel of the internal <a href="#lindas">linda</a> used for timers (this <a href="#lindas">linda</a> exists even when timers aren't enabled). |
| 1776 | Default duration is 0, which should only cause a thread context switch.<br /> | 1776 | Passing <tt>nil</tt> or no argument sleeps indefinitely (until <a href="#cancelling">cancellation</a> is received). Passing a non-negative number sleeps for that many seconds.<br /> |
| 1777 | Return values should always be <tt>nil, "timeout"</tt> (or <tt>nil, lanes.cancel_error</tt> in case of interruption). | 1777 | Return values should always be <tt>nil, "timeout"</tt> (or <tt>nil, lanes.cancel_error</tt> in case of interruption). |
| 1778 | </p> | 1778 | </p> |
| 1779 | 1779 | ||
| @@ -2138,13 +2138,13 @@ static MyDeepFactory g_MyDeepFactory; | |||
| 2138 | <h3 id="cancelling_cancel">Cancelling cancel</h3> | 2138 | <h3 id="cancelling_cancel">Cancelling cancel</h3> |
| 2139 | 2139 | ||
| 2140 | <p> | 2140 | <p> |
| 2141 | Cancellation of lanes uses the Lua error mechanism with a special lightuserdata error sentinel. | 2141 | <a href="#cancelling">Cancellation</a> of lanes uses the Lua error mechanism with a special lightuserdata error sentinel. |
| 2142 | If you use <tt>pcall</tt> in code that needs to be cancellable from the outside, the special error might not get through to Lanes, thus preventing the lane from being cleanly cancelled. | 2142 | If you use <tt>pcall</tt> in code that needs to be cancellable from the outside, the special error might not get through to Lanes, thus preventing the lane from being cleanly cancelled. |
| 2143 | You should throw any lightuserdata error further. | 2143 | You should throw any lightuserdata error further. |
| 2144 | </p> | 2144 | </p> |
| 2145 | 2145 | ||
| 2146 | <p> | 2146 | <p> |
| 2147 | This system can actually be used by application to detect cancel, do your own cancellation duties, and pass on the error so Lanes will get it. If it does not get a clean cancellation from a lane in due time, it may forcefully kill the lane. | 2147 | This system can actually be used by application to detect cancel, do your own <a href="#cancelling">cancellation</a> duties, and pass on the error so Lanes will get it. If it does not get a clean cancellation from a lane in due time, it may forcefully kill the lane. |
| 2148 | </p> | 2148 | </p> |
| 2149 | 2149 | ||
| 2150 | <p> | 2150 | <p> |
diff --git a/src/lanes.cpp b/src/lanes.cpp index ef8577b..cf227f9 100644 --- a/src/lanes.cpp +++ b/src/lanes.cpp | |||
| @@ -169,25 +169,23 @@ LUAG_FUNC(sleep) | |||
| 169 | extern LUAG_FUNC(linda_receive); | 169 | extern LUAG_FUNC(linda_receive); |
| 170 | 170 | ||
| 171 | Universe* const _U{ Universe::Get(L_) }; | 171 | Universe* const _U{ Universe::Get(L_) }; |
| 172 | lua_settop(L_, 1); | 172 | lua_settop(L_, 1); // L_: duration? |
| 173 | lua_pushcfunction(L_, LG_linda_receive); // L_: duration|nil receive() | 173 | lua_pushcfunction(L_, LG_linda_receive); // L_: duration? receive() |
| 174 | STACK_CHECK_START_REL(L_, 0); // we pushed the function we intend to call, now prepare the arguments | 174 | STACK_CHECK_START_REL(L_, 0); // we pushed the function we intend to call, now prepare the arguments |
| 175 | _U->timerLinda->push(L_); // L_: duration|nil receive() timerLinda | 175 | _U->timerLinda->push(L_); // L_: duration? receive() timerLinda |
| 176 | if (luaW_tostring(L_, StackIndex{ 1 }) == "indefinitely") { | 176 | if (lua_isnumber(L_, 1)) { |
| 177 | lua_pushnil(L_); // L_: duration? receive() timerLinda nil | ||
| 178 | } else if (lua_isnoneornil(L_, 1)) { | ||
| 179 | lua_pushnumber(L_, 0); // L_: duration? receive() timerLinda 0 | ||
| 180 | } else if (!lua_isnumber(L_, 1)) { | ||
| 181 | raise_luaL_argerror(L_, StackIndex{ 1 }, "duration must be a number"); | ||
| 182 | } | ||
| 183 | else { | ||
| 184 | auto const _n{ lua_tonumber(L_, 1) }; | 177 | auto const _n{ lua_tonumber(L_, 1) }; |
| 185 | if (_n < 0) { | 178 | if (_n < 0) { |
| 186 | raise_luaL_argerror(L_, StackIndex{ 1 }, "duration must be >= 0"); | 179 | raise_luaL_argerror(L_, StackIndex{ 1 }, "duration must be >= 0"); |
| 187 | } | 180 | } |
| 188 | lua_pushnumber(L_, lua_tonumber(L_, 1)); // L_: duration? receive() timerLinda duration | 181 | lua_pushvalue(L_, StackIndex{ 1 }); // L_: duration? receive() timerLinda timeout |
| 182 | } else if (lua_isnoneornil(L_, 1)) { | ||
| 183 | lua_pushnil(L_); // L_: duration? receive() timerLinda timeout | ||
| 184 | } else { | ||
| 185 | raise_luaL_argerror(L_, StackIndex{ 1 }, "duration must be a number"); | ||
| 189 | } | 186 | } |
| 190 | luaW_pushstring(L_, "ac100de1-a696-4619-b2f0-a26de9d58ab8"); // L_: duration? receive() timerLinda duration key | 187 | |
| 188 | luaW_pushstring(L_, "ac100de1-a696-4619-b2f0-a26de9d58ab8"); // L_: duration? receive() timerLinda timeout key | ||
| 191 | STACK_CHECK(L_, 3); // 3 arguments ready | 189 | STACK_CHECK(L_, 3); // 3 arguments ready |
| 192 | lua_call(L_, 3, LUA_MULTRET); // timerLinda:receive(duration,key) // L_: duration? result... | 190 | lua_call(L_, 3, LUA_MULTRET); // timerLinda:receive(duration,key) // L_: duration? result... |
| 193 | return lua_gettop(L_) - 1; | 191 | return lua_gettop(L_) - 1; |
diff --git a/tests/track_lanes.lua b/tests/track_lanes.lua index ef2ca06..27d5cf4 100644 --- a/tests/track_lanes.lua +++ b/tests/track_lanes.lua | |||
| @@ -46,7 +46,7 @@ end | |||
| 46 | local g = lanes.gen( "*", { name = 'auto' }, sleeper) | 46 | local g = lanes.gen( "*", { name = 'auto' }, sleeper) |
| 47 | 47 | ||
| 48 | -- start a forever-waiting lane (nil timeout) | 48 | -- start a forever-waiting lane (nil timeout) |
| 49 | local forever = g( "forever", 'indefinitely') | 49 | local forever = g( "forever", nil) |
| 50 | 50 | ||
| 51 | -- start a lane that will last 2 seconds | 51 | -- start a lane that will last 2 seconds |
| 52 | local ephemeral1 = g( "two_seconds", 2) | 52 | local ephemeral1 = g( "two_seconds", 2) |
diff --git a/unit_tests/lane_tests.cpp b/unit_tests/lane_tests.cpp index b6fb188..7c215fa 100644 --- a/unit_tests/lane_tests.cpp +++ b/unit_tests/lane_tests.cpp | |||
| @@ -128,9 +128,11 @@ TEST_CASE("lanes.sleep.argument_validation/numbers") | |||
| 128 | // negative durations are not supported | 128 | // negative durations are not supported |
| 129 | S.requireFailure("lanes.sleep(-1)"); | 129 | S.requireFailure("lanes.sleep(-1)"); |
| 130 | 130 | ||
| 131 | // no duration is supported (same as 0) | 131 | // no duration is supported |
| 132 | S.requireSuccess("lanes.sleep()"); | ||
| 133 | S.requireSuccess("lanes.sleep(0)"); | 132 | S.requireSuccess("lanes.sleep(0)"); |
| 133 | |||
| 134 | // positive durations are supported | ||
| 135 | S.requireSuccess("lanes.sleep(0.1)"); | ||
| 134 | } | 136 | } |
| 135 | 137 | ||
| 136 | // ################################################################################################# | 138 | // ################################################################################################# |
| @@ -161,7 +163,7 @@ TEST_CASE("lanes.sleep.interactions with timers") | |||
| 161 | " lanes.timer(l, 'gluh', 0.1, 0.1)" | 163 | " lanes.timer(l, 'gluh', 0.1, 0.1)" |
| 162 | // launch a lane that is supposed to sleep forever | 164 | // launch a lane that is supposed to sleep forever |
| 163 | " local g = lanes.gen('*', { name = 'auto' }, lanes.sleep)" | 165 | " local g = lanes.gen('*', { name = 'auto' }, lanes.sleep)" |
| 164 | " local h = g('indefinitely')" | 166 | " local h = g(nil)" |
| 165 | // sleep 1 second (this uses the timer linda) | 167 | // sleep 1 second (this uses the timer linda) |
| 166 | " lanes.sleep(1)" | 168 | " lanes.sleep(1)" |
| 167 | // shutdown should be able to cancel the lane and stop it instantly | 169 | // shutdown should be able to cancel the lane and stop it instantly |
