aboutsummaryrefslogtreecommitdiff
path: root/src/universe.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/universe.cpp')
-rw-r--r--src/universe.cpp105
1 files changed, 62 insertions, 43 deletions
diff --git a/src/universe.cpp b/src/universe.cpp
index 335f056..4db036b 100644
--- a/src/universe.cpp
+++ b/src/universe.cpp
@@ -51,6 +51,16 @@ static constexpr RegistryUniqueKey kUniverseFullRegKey{ 0x1C2D76870DD9DD9Full };
51 51
52// ################################################################################################# 52// #################################################################################################
53 53
54[[nodiscard]]
55void* ProtectedAllocator::Protected_lua_Alloc(void* const ud_, void* const ptr_, size_t const osize_, size_t const nsize_)
56{
57 ProtectedAllocator* const _allocator{ static_cast<ProtectedAllocator*>(ud_) };
58 std::lock_guard<std::mutex> _guard{ _allocator->mutex };
59 return _allocator->alloc(ptr_, osize_, nsize_);
60}
61
62// #################################################################################################
63
54Universe::Universe() 64Universe::Universe()
55{ 65{
56 //--- 66 //---
@@ -101,18 +111,18 @@ void Universe::callOnStateCreate(lua_State* const L_, lua_State* const from_, Lo
101 } 111 }
102 kConfigRegKey.pushValue(L_); // L_: config 112 kConfigRegKey.pushValue(L_); // L_: config
103 STACK_CHECK(L_, 1); 113 STACK_CHECK(L_, 1);
104 LuaType const _funcType{ luaG_getfield(L_, kIdxTop, kOnStateCreate) }; // L_: config on_state_create() 114 LuaType const _funcType{ luaW_getfield(L_, kIdxTop, kOnStateCreate) }; // L_: config on_state_create()
105 if (_funcType != LuaType::FUNCTION) { 115 if (_funcType != LuaType::FUNCTION) {
106 raise_luaL_error(L_, "INTERNAL ERROR: %s is a %s, not a function", kOnStateCreate.data(), luaG_typename(L_, _funcType).data()); 116 raise_luaL_error(L_, "INTERNAL ERROR: %s is a %s, not a function", kOnStateCreate.data(), luaW_typename(L_, _funcType).data());
107 } 117 }
108 lua_remove(L_, -2); // L_: on_state_create() 118 lua_remove(L_, -2); // L_: on_state_create()
109 } 119 }
110 STACK_CHECK(L_, 1); 120 STACK_CHECK(L_, 1);
111 // capture error and raise it in caller state 121 // capture error and raise it in caller state
112 std::string_view const _stateType{ mode_ == LookupMode::LaneBody ? "lane" : "keeper" }; 122 std::string_view const _stateType{ mode_ == LookupMode::LaneBody ? "lane" : "keeper" };
113 luaG_pushstring(L_, _stateType); // L_: on_state_create() "<type>" 123 luaW_pushstring(L_, _stateType); // L_: on_state_create() "<type>"
114 if (lua_pcall(L_, 1, 0, 0) != LUA_OK) { 124 if (lua_pcall(L_, 1, 0, 0) != LUA_OK) {
115 raise_luaL_error(from_, "%s failed in %s: \"%s\"", kOnStateCreate.data(), _stateType.data(), lua_isstring(L_, -1) ? luaG_tostring(L_, kIdxTop).data() : luaG_typename(L_, kIdxTop).data()); 125 raise_luaL_error(from_, "%s failed in %s: \"%s\"", kOnStateCreate.data(), _stateType.data(), lua_isstring(L_, -1) ? luaW_tostring(L_, kIdxTop).data() : luaW_typename(L_, kIdxTop).data());
116 } 126 }
117 STACK_CHECK(L_, 0); 127 STACK_CHECK(L_, 0);
118} 128}
@@ -127,14 +137,14 @@ Universe* Universe::Create(lua_State* const L_)
127 static constexpr StackIndex kIdxSettings{ 1 }; 137 static constexpr StackIndex kIdxSettings{ 1 };
128 LUA_ASSERT(L_, lua_gettop(L_) == 1 && lua_istable(L_, 1)); 138 LUA_ASSERT(L_, lua_gettop(L_) == 1 && lua_istable(L_, 1));
129 STACK_CHECK_START_REL(L_, 0); // L_: settings 139 STACK_CHECK_START_REL(L_, 0); // L_: settings
130 std::ignore = luaG_getfield(L_, kIdxSettings, "nb_user_keepers"); // L_: settings nb_user_keepers 140 std::ignore = luaW_getfield(L_, kIdxSettings, "nb_user_keepers"); // L_: settings nb_user_keepers
131 int const _nbUserKeepers{ static_cast<int>(lua_tointeger(L_, -1)) + 1}; 141 int const _nbUserKeepers{ static_cast<int>(lua_tointeger(L_, -1)) + 1};
132 lua_pop(L_, 1); // L_: settings 142 lua_pop(L_, 1); // L_: settings
133 if (_nbUserKeepers < 1) { 143 if (_nbUserKeepers < 1) {
134 raise_luaL_error(L_, "Bad number of additional keepers (%d)", _nbUserKeepers); 144 raise_luaL_error(L_, "Bad number of additional keepers (%d)", _nbUserKeepers);
135 } 145 }
136 STACK_CHECK(L_, 0); 146 STACK_CHECK(L_, 0);
137 std::ignore = luaG_getfield(L_, kIdxSettings, "keepers_gc_threshold"); // L_: settings keepers_gc_threshold 147 std::ignore = luaW_getfield(L_, kIdxSettings, "keepers_gc_threshold"); // L_: settings keepers_gc_threshold
138 int const _keepers_gc_threshold{ static_cast<int>(lua_tointeger(L_, -1)) }; 148 int const _keepers_gc_threshold{ static_cast<int>(lua_tointeger(L_, -1)) };
139 lua_pop(L_, 1); // L_: settings 149 lua_pop(L_, 1); // L_: settings
140 STACK_CHECK(L_, 0); 150 STACK_CHECK(L_, 0);
@@ -147,30 +157,30 @@ Universe* Universe::Create(lua_State* const L_)
147 157
148 DEBUGSPEW_CODE(DebugSpewIndentScope _scope{ _U }); 158 DEBUGSPEW_CODE(DebugSpewIndentScope _scope{ _U });
149 lua_createtable(L_, 0, 1); // L_: settings universe {mt} 159 lua_createtable(L_, 0, 1); // L_: settings universe {mt}
150 std::ignore = luaG_getfield(L_, kIdxSettings, "shutdown_timeout"); // L_: settings universe {mt} shutdown_timeout 160 std::ignore = luaW_getfield(L_, kIdxSettings, "shutdown_timeout"); // L_: settings universe {mt} shutdown_timeout
151 lua_pushcclosure(L_, UniverseGC, 1); // L_: settings universe {mt} UniverseGC 161 lua_pushcclosure(L_, UniverseGC, 1); // L_: settings universe {mt} UniverseGC
152 lua_setfield(L_, -2, "__gc"); // L_: settings universe {mt} 162 lua_setfield(L_, -2, "__gc"); // L_: settings universe {mt}
153 lua_setmetatable(L_, -2); // L_: settings universe 163 lua_setmetatable(L_, -2); // L_: settings universe
154 lua_pop(L_, 1); // L_: settings 164 lua_pop(L_, 1); // L_: settings
155 165
156 std::ignore = luaG_getfield(L_, kIdxSettings, "linda_wake_period"); // L_: settings linda_wake_period 166 std::ignore = luaW_getfield(L_, kIdxSettings, "linda_wake_period"); // L_: settings linda_wake_period
157 if (luaG_type(L_, kIdxTop) == LuaType::NUMBER) { 167 if (luaW_type(L_, kIdxTop) == LuaType::NUMBER) {
158 _U->lindaWakePeriod = lua_Duration{ lua_tonumber(L_, kIdxTop) }; 168 _U->lindaWakePeriod = lua_Duration{ lua_tonumber(L_, kIdxTop) };
159 } else { 169 } else {
160 LUA_ASSERT(L_, luaG_tostring(L_, kIdxTop) == "never"); 170 LUA_ASSERT(L_, luaW_tostring(L_, kIdxTop) == "never");
161 } 171 }
162 lua_pop(L_, 1); // L_: settings 172 lua_pop(L_, 1); // L_: settings
163 173
164 std::ignore = luaG_getfield(L_, kIdxSettings, "strip_functions"); // L_: settings strip_functions 174 std::ignore = luaW_getfield(L_, kIdxSettings, "strip_functions"); // L_: settings strip_functions
165 _U->stripFunctions = lua_toboolean(L_, -1) ? true : false; 175 _U->stripFunctions = lua_toboolean(L_, -1) ? true : false;
166 lua_pop(L_, 1); // L_: settings 176 lua_pop(L_, 1); // L_: settings
167 177
168 std::ignore = luaG_getfield(L_, kIdxSettings, "verbose_errors"); // L_: settings verbose_errors 178 std::ignore = luaW_getfield(L_, kIdxSettings, "verbose_errors"); // L_: settings verbose_errors
169 _U->verboseErrors = lua_toboolean(L_, -1) ? true : false; 179 _U->verboseErrors = lua_toboolean(L_, -1) ? true : false;
170 lua_pop(L_, 1); // L_: settings 180 lua_pop(L_, 1); // L_: settings
171 181
172 // tracking 182 // tracking
173 std::ignore = luaG_getfield(L_, kIdxSettings, "track_lanes"); // L_: settings track_lanes 183 std::ignore = luaW_getfield(L_, kIdxSettings, "track_lanes"); // L_: settings track_lanes
174 if (lua_toboolean(L_, -1)) { 184 if (lua_toboolean(L_, -1)) {
175 _U->tracker.activate(); 185 _U->tracker.activate();
176 } 186 }
@@ -178,8 +188,8 @@ Universe* Universe::Create(lua_State* const L_)
178 188
179 // Linked chains handling 189 // Linked chains handling
180 _U->selfdestructFirst = SELFDESTRUCT_END; 190 _U->selfdestructFirst = SELFDESTRUCT_END;
181 _U->initializeAllocatorFunction(L_); 191 _U->initializeAllocatorFunction(L_); // this can raise an error
182 _U->initializeOnStateCreate(L_); 192 _U->initializeOnStateCreate(L_); // this can raise an error
183 _U->keepers.initialize(*_U, L_, static_cast<size_t>(_nbUserKeepers), _keepers_gc_threshold); 193 _U->keepers.initialize(*_U, L_, static_cast<size_t>(_nbUserKeepers), _keepers_gc_threshold);
184 STACK_CHECK(L_, 0); 194 STACK_CHECK(L_, 0);
185 195
@@ -207,7 +217,7 @@ static void* libc_lua_Alloc([[maybe_unused]] void* const ud_, [[maybe_unused]] v
207// ################################################################################################# 217// #################################################################################################
208 218
209[[nodiscard]] 219[[nodiscard]]
210static int luaG_provide_protected_allocator(lua_State* const L_) 220static int luaW_provide_protected_allocator(lua_State* const L_)
211{ 221{
212 Universe* const _U{ Universe::Get(L_) }; 222 Universe* const _U{ Universe::Get(L_) };
213 // push a new full userdata on the stack, giving access to the universe's protected allocator 223 // push a new full userdata on the stack, giving access to the universe's protected allocator
@@ -217,9 +227,9 @@ static int luaG_provide_protected_allocator(lua_State* const L_)
217 227
218// ################################################################################################# 228// #################################################################################################
219 229
230// already called under protection of selfdestructMutex
220void Universe::flagDanglingLanes() const 231void Universe::flagDanglingLanes() const
221{ 232{
222 std::lock_guard<std::mutex> _guard{ selfdestructMutex };
223 Lane* _lane{ selfdestructFirst }; 233 Lane* _lane{ selfdestructFirst };
224 while (_lane != SELFDESTRUCT_END) { 234 while (_lane != SELFDESTRUCT_END) {
225 _lane->flaggedAfterUniverseGC.store(true, std::memory_order_relaxed); 235 _lane->flaggedAfterUniverseGC.store(true, std::memory_order_relaxed);
@@ -235,17 +245,17 @@ void Universe::initializeAllocatorFunction(lua_State* const L_)
235 // start by just grabbing whatever allocator was provided to the master state 245 // start by just grabbing whatever allocator was provided to the master state
236 protectedAllocator.initFrom(L_); 246 protectedAllocator.initFrom(L_);
237 STACK_CHECK_START_REL(L_, 1); // L_: settings 247 STACK_CHECK_START_REL(L_, 1); // L_: settings
238 switch (luaG_getfield(L_, kIdxTop, "allocator")) { // L_: settings allocator|nil|"protected" 248 switch (luaW_getfield(L_, kIdxTop, "allocator")) { // L_: settings allocator|nil|"protected"
239 case LuaType::NIL: 249 case LuaType::NIL:
240 // nothing else to do 250 // nothing else to do
241 break; 251 break;
242 252
243 case LuaType::STRING: 253 case LuaType::STRING:
244 LUA_ASSERT(L_, luaG_tostring(L_, kIdxTop) == "protected"); 254 LUA_ASSERT(L_, luaW_tostring(L_, kIdxTop) == "protected");
245 // set the original allocator to call from inside protection by the mutex 255 // set the original allocator to call from inside protection by the mutex
246 protectedAllocator.installIn(L_); 256 protectedAllocator.installIn(L_);
247 // before a state is created, this function will be called to obtain the allocator 257 // before a state is created, this function will be called to obtain the allocator
248 provideAllocator = luaG_provide_protected_allocator; 258 provideAllocator = luaW_provide_protected_allocator;
249 break; 259 break;
250 260
251 case LuaType::FUNCTION: 261 case LuaType::FUNCTION:
@@ -266,14 +276,14 @@ void Universe::initializeAllocatorFunction(lua_State* const L_)
266 break; 276 break;
267 277
268 default: // should be filtered out in lanes.lua 278 default: // should be filtered out in lanes.lua
269 raise_luaL_error(L_, "Bad config.allocator type %s", luaG_typename(L_, kIdxTop).data()); 279 raise_luaL_error(L_, "Bad config.allocator type %s", luaW_typename(L_, kIdxTop).data());
270 } 280 }
271 lua_pop(L_, 1); // L_: settings 281 lua_pop(L_, 1); // L_: settings
272 STACK_CHECK(L_, 1); 282 STACK_CHECK(L_, 1);
273 283
274 std::ignore = luaG_getfield(L_, kIdxTop, "internal_allocator"); // L_: settings "libc"|"allocator" 284 std::ignore = luaW_getfield(L_, kIdxTop, "internal_allocator"); // L_: settings "libc"|"allocator"
275 LUA_ASSERT(L_, lua_isstring(L_, kIdxTop)); // should be the case due to lanes.lua parameter validation 285 LUA_ASSERT(L_, lua_isstring(L_, kIdxTop)); // should be the case due to lanes.lua parameter validation
276 std::string_view const _allocator{ luaG_tostring(L_, kIdxTop) }; 286 std::string_view const _allocator{ luaW_tostring(L_, kIdxTop) };
277 // use whatever the provider provides. This performs validation of what provideAllocator is giving 287 // use whatever the provider provides. This performs validation of what provideAllocator is giving
278 // we do this even if _allocator == "libc", to have the validation part 288 // we do this even if _allocator == "libc", to have the validation part
279 internalAllocator = resolveAndValidateAllocator(L_, "internal"); 289 internalAllocator = resolveAndValidateAllocator(L_, "internal");
@@ -295,7 +305,7 @@ int Universe::InitializeFinalizer(lua_State* const L_)
295 305
296 // make sure we are only called from the Master Lua State! 306 // make sure we are only called from the Master Lua State!
297 kUniverseFullRegKey.pushValue(L_); // L_: f U 307 kUniverseFullRegKey.pushValue(L_); // L_: f U
298 if (luaG_type(L_, kIdxTop) != LuaType::USERDATA) { 308 if (luaW_type(L_, kIdxTop) != LuaType::USERDATA) {
299 raise_luaL_error(L_, "lanes.%s called from inside a lane", kFinally); 309 raise_luaL_error(L_, "lanes.%s called from inside a lane", kFinally);
300 } 310 }
301 lua_pop(L_, 1); // L_: f 311 lua_pop(L_, 1); // L_: f
@@ -311,8 +321,8 @@ int Universe::InitializeFinalizer(lua_State* const L_)
311void Universe::initializeOnStateCreate(lua_State* const L_) 321void Universe::initializeOnStateCreate(lua_State* const L_)
312{ 322{
313 STACK_CHECK_START_REL(L_, 0); // L_: settings 323 STACK_CHECK_START_REL(L_, 0); // L_: settings
314 if (luaG_getfield(L_, kIdxTop, kOnStateCreate) != LuaType::NIL) { // L_: settings on_state_create|nil 324 if (luaW_getfield(L_, kIdxTop, kOnStateCreate) != LuaType::NIL) { // L_: settings on_state_create|nil
315 LUA_ASSERT(L_, luaG_type(L_, kIdxTop) == LuaType::FUNCTION); // ensured by lanes.lua parameter validation 325 LUA_ASSERT(L_, luaW_type(L_, kIdxTop) == LuaType::FUNCTION); // ensured by lanes.lua parameter validation
316 // store C function pointer in an internal variable 326 // store C function pointer in an internal variable
317 lua_CFunction const _func{ lua_tocfunction(L_, -1) }; // L_: settings on_state_create 327 lua_CFunction const _func{ lua_tocfunction(L_, -1) }; // L_: settings on_state_create
318 if (_func) { 328 if (_func) {
@@ -325,7 +335,7 @@ void Universe::initializeOnStateCreate(lua_State* const L_)
325 // remove this C function from the config table so that it doesn't cause problems 335 // remove this C function from the config table so that it doesn't cause problems
326 // when we transfer the config table in newly created Lua states 336 // when we transfer the config table in newly created Lua states
327 lua_pushnil(L_); // L_: settings on_state_create nil 337 lua_pushnil(L_); // L_: settings on_state_create nil
328 luaG_setfield(L_, StackIndex{ -3 }, kOnStateCreate); // L_: settings on_state_create 338 luaW_setfield(L_, StackIndex{ -3 }, kOnStateCreate); // L_: settings on_state_create
329 } else { 339 } else {
330 // the function is still in the config table. we indicate this with the uintptr_t alternative (actual value is irrelevant) 340 // the function is still in the config table. we indicate this with the uintptr_t alternative (actual value is irrelevant)
331 onStateCreateFunc.emplace<uintptr_t>(std::bit_cast<uintptr_t>(kOnStateCreate.data())); 341 onStateCreateFunc.emplace<uintptr_t>(std::bit_cast<uintptr_t>(kOnStateCreate.data()));
@@ -348,7 +358,7 @@ lanes::AllocatorDefinition Universe::resolveAndValidateAllocator(lua_State* cons
348 358
349 STACK_CHECK_START_REL(L_, 0); // here, we have a function we can call to obtain an allocator 359 STACK_CHECK_START_REL(L_, 0); // here, we have a function we can call to obtain an allocator
350 lua_pushcclosure(L_, provideAllocator, 0); // L_: provideAllocator() 360 lua_pushcclosure(L_, provideAllocator, 0); // L_: provideAllocator()
351 luaG_pushstring(L_, hint_); // L_: provideAllocator() "<hint>" 361 luaW_pushstring(L_, hint_); // L_: provideAllocator() "<hint>"
352 lua_call(L_, 1, 1); // L_: result 362 lua_call(L_, 1, 1); // L_: result
353 // make sure we have a valid AllocatorDefinition on the stack (an error is raised instead if it is not the case) 363 // make sure we have a valid AllocatorDefinition on the stack (an error is raised instead if it is not the case)
354 _ret = lanes::AllocatorDefinition::Validated(L_, kIdxTop); 364 _ret = lanes::AllocatorDefinition::Validated(L_, kIdxTop);
@@ -423,7 +433,7 @@ int Universe::UniverseGC(lua_State* const L_)
423{ 433{
424 lua_Duration const _shutdown_timeout{ lua_tonumber(L_, lua_upvalueindex(1)) }; 434 lua_Duration const _shutdown_timeout{ lua_tonumber(L_, lua_upvalueindex(1)) };
425 STACK_CHECK_START_ABS(L_, 1); 435 STACK_CHECK_START_ABS(L_, 1);
426 Universe* const _U{ luaG_tofulluserdata<Universe>(L_, StackIndex{ 1 }) }; // L_: U 436 Universe* const _U{ luaW_tofulluserdata<Universe>(L_, StackIndex{ 1 }) }; // L_: U
427 437
428 // attempt to terminate all lanes with increasingly stronger cancel methods 438 // attempt to terminate all lanes with increasingly stronger cancel methods
429 bool const _allLanesTerminated{ 439 bool const _allLanesTerminated{
@@ -437,7 +447,7 @@ int Universe::UniverseGC(lua_State* const L_)
437 if (!lua_isnil(L_, -1)) { 447 if (!lua_isnil(L_, -1)) {
438 lua_pushboolean(L_, _allLanesTerminated); // L_: U finalizer bool 448 lua_pushboolean(L_, _allLanesTerminated); // L_: U finalizer bool
439 // no protection. Lua rules for errors in finalizers apply normally: 449 // no protection. Lua rules for errors in finalizers apply normally:
440 // Lua 5.4: error is propagated in the warn system 450 // Lua 5.4+: error is propagated in the warn system
441 // older: error is swallowed 451 // older: error is swallowed
442 lua_call(L_, 1, 1); // L_: U msg? 452 lua_call(L_, 1, 1); // L_: U msg?
443 // phew, no error in finalizer, since we reached that point 453 // phew, no error in finalizer, since we reached that point
@@ -446,24 +456,31 @@ int Universe::UniverseGC(lua_State* const L_)
446 if (lua_isnil(L_, kIdxTop)) { 456 if (lua_isnil(L_, kIdxTop)) {
447 lua_pop(L_, 1); // L_: U 457 lua_pop(L_, 1); // L_: U
448 // no finalizer, or it returned no value: push some default message on the stack, in case it is necessary 458 // no finalizer, or it returned no value: push some default message on the stack, in case it is necessary
449 luaG_pushstring(L_, "uncooperative lanes detected at shutdown"); // L_: U "msg" 459 luaW_pushstring(L_, "uncooperative lanes detected at shutdown"); // L_: U "msg"
450 } 460 }
451 STACK_CHECK(L_, 2); 461 STACK_CHECK(L_, 2);
452 462
453 // now, all remaining lanes are flagged. if they crash because we remove keepers and the Universe from under them, it is their fault 463 {
454 bool const _detectedUncooperativeLanes{ _U->selfdestructFirst != SELFDESTRUCT_END }; 464 std::lock_guard<std::mutex> _guard{ _U->selfdestructMutex };
455 if (_detectedUncooperativeLanes) { 465 // now, all remaining lanes are flagged. if they crash because we remove keepers and the Universe from under them, it is their fault
456 _U->flagDanglingLanes(); 466 bool const _detectedUncooperativeLanes{ _U->selfdestructFirst != SELFDESTRUCT_END };
457 if (luaG_tostring(L_, kIdxTop) == "freeze") { 467 if (_detectedUncooperativeLanes) {
458 std::this_thread::sleep_until(std::chrono::time_point<std::chrono::steady_clock>::max()); 468 _U->flagDanglingLanes();
469 if (luaW_tostring(L_, kIdxTop) == "freeze") {
470 std::this_thread::sleep_until(std::chrono::time_point<std::chrono::steady_clock>::max());
471 } else {
472 // take the value returned by the finalizer (or our default message) and throw it as an error
473 // since we are inside Lua's GCTM, it will be propagated through the warning system (Lua 5.4) or swallowed silently
474 // IMPORTANT: lua_error() is used here instead of the wrapper raise_lua_error() to circumvent what looks like a MSVC compiler bug
475 // that manifests as a crash inside ntdll!longjmp() function, in optimized builds only
476 lua_error(L_);
477 }
459 } else { 478 } else {
460 // take the value returned by the finalizer (or our default message) and throw it as an error 479 // we didn't use the error message, let's keep a clean stack
461 // since we are inside Lua's GCTM, it will be propagated through the warning system (Lua 5.4) or swallowed silently 480 lua_pop(L_, 1); // L_: U
462 // IMPORTANT: lua_error() is used here instead of the wrapper raise_lua_error() to circumvent what looks like a MSVC compiler bug
463 // that manifests as a crash inside ntdll!longjmp() function, in optimized builds only
464 lua_error(L_);
465 } 481 }
466 } 482 }
483 STACK_CHECK(L_, 1);
467 484
468 // --------------------------------------------------------- 485 // ---------------------------------------------------------
469 // we don't reach that point if some lanes are still running 486 // we don't reach that point if some lanes are still running
@@ -472,7 +489,9 @@ int Universe::UniverseGC(lua_State* const L_)
472 // no need to mutex-protect this as all lanes in the universe are gone at that point 489 // no need to mutex-protect this as all lanes in the universe are gone at that point
473 Linda::DeleteTimerLinda(L_, std::exchange(_U->timerLinda, nullptr), PK); 490 Linda::DeleteTimerLinda(L_, std::exchange(_U->timerLinda, nullptr), PK);
474 491
475 _U->keepers.close(); 492 if (!_U->keepers.close()) {
493 raise_luaL_error(L_, "INTERNAL ERROR: Keepers closed more than once");
494 }
476 495
477 // remove the protected allocator, if any 496 // remove the protected allocator, if any
478 _U->protectedAllocator.removeFrom(L_); 497 _U->protectedAllocator.removeFrom(L_);