2007-01-03 03:11:53 +00:00
|
|
|
/*! \file timer.cpp
|
|
|
|
|
* \brief Mini-task scheduler for timed events.
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
|
2006-09-01 22:59:23 +00:00
|
|
|
#include "copyright.h"
|
|
|
|
|
#include "autoconf.h"
|
|
|
|
|
#include "config.h"
|
|
|
|
|
#include "externs.h"
|
2026-07-31 09:09:18 -06:00
|
|
|
#include <new>
|
fix(engine,net): contain exceptions on the command/task path (#2009)
Everything softcode does runs under CScheduler::RunTasks -- command
parsing, function evaluation, mail, comsys, @dump -- and all of it
allocates. There was no catch between that frame and main(), so a
throw became std::terminate -> abort -> SIGABRT, and signals.cpp
handles SIGABRT by logging and exit(1): no dump_restart_db(), no
re-exec. A SIGSEGV on the same line forks, dumps and execl()s a fresh
netmux, so an exception cost the database where a null dereference
would have self-healed.
The issue treated 2.14 reachability as inferred, since its gdb trace
came from 2.13. It is now demonstrated here: with a one-shot
bad_alloc injected into ConnectionBase::close() and a login failed
past retry_limit, unpatched master dies with Abort trap: 6 and refuses
further connections.
Three barriers.
1. Per task, inside CScheduler::RunTasks. Chosen over per-tick
containment for two reasons. One bad command dies without dropping
the rest of the tick. And it keeps the delete reachable: pTask is
already off the priority heap when fpTask runs, so an escaping
throw leaked the record as well as losing the task -- per-tick
containment cannot fix that.
Abandoning one task is survivable. process_command() resets
func_nest_lev, func_invk_ctr, ntfy_nest_lev and lock_nest_lev at
the top of every command, so a half-finished command cannot poison
the next one's limits. It leaks that command's lbufs, which is
bounded and far cheaper than losing the database.
2. A netmux-side backstop around g_pIGameEngine->RunTasks. The
per-task barrier lives in engine.so while run_main_loop is in
netmux -- a module boundary 2.13 does not have, since it reaches
RunTasks directly. The backstop covers a throw from the
scheduler's own frame (the heap operations around the task record)
and anything crossing the boundary whose type the per-task arms did
not match: engine.so is built -fvisibility=hidden, so an
engine-local exception type has no exported typeinfo in netmux and
lands in catch (...).
3. close_contained() for the two conn->close() calls inside the
existing handleNetworkEvent catch arms. They were bare, and a
throw from close() is the demonstrated trigger, so they would have
escaped the handler and aborted anyway. A barrier that can abort
is not a barrier. Abandoning the close leaves the connection to be
reaped by the ordinary idle path, which is survivable.
Verified by injection on macOS arm64, baseline first so that a pass
could not be vacuous -- the close only fires once retries_left reaches
zero (retry_limit defaults to 3), so a single bad connect would have
proved nothing:
unpatched attempt 2: connection closed; Abort trap: 6
DEAD: cannot connect (Errno 61); pid gone
with fix attempt 3: connection closed cleanly
ALIVE: server answered 725 bytes
MUX BUG/TASK : Exception escaped a scheduled task
(std::bad_alloc); task abandoned.
The liveness check is a follow-up connection that must be answered,
not merely accepted; a listening socket outlives a dying process.
That also exercises cross-binary propagation: the throw originates in
libganl, is caught in engine.so, and netmux continues -- under
libc++abi on arm64, a different unwinder and architecture from the
Linux x86-64 run on the 2.13 side.
make test: 33 passed, 1 skipped (stubslave, not configured), 0 failed.
2026-08-03 16:13:54 -06:00
|
|
|
#include <exception>
|
2006-09-01 22:59:23 +00:00
|
|
|
|
|
|
|
|
CScheduler scheduler;
|
|
|
|
|
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
// Run one recurring system task's body, containing any exception (#2011).
|
|
|
|
|
//
|
|
|
|
|
// #2009 stopped a throwing task from killing the game. It did not stop a
|
|
|
|
|
// throwing task from killing ITSELF: every dispatcher below re-defers its
|
|
|
|
|
// next run on its LAST line, and the #2009 barrier unwinds straight past
|
|
|
|
|
// that line, so the task is never scheduled again and silently stops
|
|
|
|
|
// forever.
|
|
|
|
|
//
|
|
|
|
|
// Measured with a one-shot throw on the third cache tick at
|
|
|
|
|
// cache_tick_period 1: three ticks in thirty seconds, then nothing. With
|
|
|
|
|
// the body wrapped, twenty-nine.
|
|
|
|
|
//
|
|
|
|
|
// For dispatch_DatabaseDump that difference is the whole point -- a game
|
|
|
|
|
// that survives the exception and then quietly stops dumping is losing the
|
|
|
|
|
// database slowly instead of all at once, which is not the trade #2009 was
|
|
|
|
|
// meant to buy.
|
|
|
|
|
//
|
|
|
|
|
// Wrapping only the body leaves each reschedule tail unconditional.
|
|
|
|
|
//
|
|
|
|
|
template<typename F>
|
|
|
|
|
static void run_task_body(F body, const UTF8 *pWhich)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
body();
|
|
|
|
|
}
|
|
|
|
|
catch (const std::exception &e)
|
|
|
|
|
{
|
|
|
|
|
STARTLOG(LOG_BUGS, T("BUG"), T("TASK"));
|
|
|
|
|
log_printf(T("Exception in system task %s (%s); rescheduled anyway."),
|
|
|
|
|
pWhich, reinterpret_cast<const UTF8 *>(e.what()));
|
|
|
|
|
ENDLOG;
|
|
|
|
|
}
|
|
|
|
|
catch (...)
|
|
|
|
|
{
|
|
|
|
|
STARTLOG(LOG_BUGS, T("BUG"), T("TASK"));
|
|
|
|
|
log_printf(T("Unknown exception in system task %s; rescheduled anyway."),
|
|
|
|
|
pWhich);
|
|
|
|
|
ENDLOG;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2006-09-01 22:59:23 +00:00
|
|
|
// Free List Reconstruction Task routine.
|
|
|
|
|
//
|
|
|
|
|
void dispatch_FreeListReconstruction(void *pUnused, int iUnused)
|
|
|
|
|
{
|
|
|
|
|
UNUSED_PARAMETER(pUnused);
|
|
|
|
|
UNUSED_PARAMETER(iUnused);
|
|
|
|
|
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
run_task_body([]()
|
2006-09-01 22:59:23 +00:00
|
|
|
{
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
if (mudconf.control_flags & CF_DBCHECK)
|
|
|
|
|
{
|
|
|
|
|
const UTF8 *cmdsave = g_debug_cmd;
|
|
|
|
|
g_debug_cmd = T("< dbck >");
|
|
|
|
|
do_dbck(NOTHING, NOTHING, NOTHING, 0, 0);
|
|
|
|
|
Guest.CleanUp();
|
|
|
|
|
pcache_trim();
|
|
|
|
|
pool_reset();
|
|
|
|
|
g_debug_cmd = cmdsave;
|
|
|
|
|
}
|
|
|
|
|
}, T("dbck"));
|
2006-09-01 22:59:23 +00:00
|
|
|
|
|
|
|
|
// Schedule ourselves again.
|
|
|
|
|
//
|
|
|
|
|
CLinearTimeAbsolute ltaNow;
|
|
|
|
|
ltaNow.GetUTC();
|
|
|
|
|
CLinearTimeDelta ltd;
|
|
|
|
|
ltd.SetSeconds(mudconf.check_interval);
|
|
|
|
|
mudstate.check_counter = ltaNow + ltd;
|
|
|
|
|
scheduler.DeferTask(mudstate.check_counter, PRIORITY_SYSTEM,
|
|
|
|
|
dispatch_FreeListReconstruction, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Database Dump Task routine.
|
|
|
|
|
//
|
|
|
|
|
void dispatch_DatabaseDump(void *pUnused, int iUnused)
|
|
|
|
|
{
|
|
|
|
|
UNUSED_PARAMETER(pUnused);
|
|
|
|
|
UNUSED_PARAMETER(iUnused);
|
|
|
|
|
|
|
|
|
|
int nNextTimeInSeconds = mudconf.dump_interval;
|
|
|
|
|
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
run_task_body([&nNextTimeInSeconds]()
|
2006-09-01 22:59:23 +00:00
|
|
|
{
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
if (mudconf.control_flags & CF_CHECKPOINT)
|
2006-09-01 22:59:23 +00:00
|
|
|
{
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
const UTF8 *cmdsave = g_debug_cmd;
|
|
|
|
|
g_debug_cmd = T("< dump >");
|
|
|
|
|
#if defined(HAVE_WORKING_FORK)
|
|
|
|
|
if (mudstate.dumping)
|
|
|
|
|
{
|
|
|
|
|
// There is a dump in progress. These usually happen very
|
|
|
|
|
// quickly. We will reschedule ourselves to try again in 20
|
|
|
|
|
// seconds. Ordinarily, you would think "...a dump is a
|
|
|
|
|
// dump...", but some dumps might not be the type of dump
|
|
|
|
|
// we're going to do.
|
|
|
|
|
//
|
|
|
|
|
nNextTimeInSeconds = 20;
|
|
|
|
|
}
|
|
|
|
|
else
|
2007-12-26 08:17:12 +00:00
|
|
|
#endif // HAVE_WORKING_FORK
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
{
|
|
|
|
|
fork_and_dump(0);
|
|
|
|
|
}
|
|
|
|
|
g_debug_cmd = cmdsave;
|
2006-09-01 22:59:23 +00:00
|
|
|
}
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
}, T("dump"));
|
2006-09-01 22:59:23 +00:00
|
|
|
|
|
|
|
|
// Schedule ourselves again.
|
|
|
|
|
//
|
|
|
|
|
CLinearTimeAbsolute ltaNow;
|
|
|
|
|
ltaNow.GetUTC();
|
|
|
|
|
CLinearTimeDelta ltd;
|
|
|
|
|
ltd.SetSeconds(nNextTimeInSeconds);
|
|
|
|
|
mudstate.dump_counter = ltaNow + ltd;
|
|
|
|
|
scheduler.DeferTask(mudstate.dump_counter, PRIORITY_SYSTEM, dispatch_DatabaseDump, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Idle Check Task routine.
|
|
|
|
|
//
|
|
|
|
|
void dispatch_IdleCheck(void *pUnused, int iUnused)
|
|
|
|
|
{
|
|
|
|
|
UNUSED_PARAMETER(pUnused);
|
|
|
|
|
UNUSED_PARAMETER(iUnused);
|
|
|
|
|
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
run_task_body([]()
|
2006-09-01 22:59:23 +00:00
|
|
|
{
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
if (mudconf.control_flags & CF_IDLECHECK)
|
|
|
|
|
{
|
|
|
|
|
const UTF8 *cmdsave = g_debug_cmd;
|
|
|
|
|
g_debug_cmd = T("< idlecheck >");
|
|
|
|
|
check_idle();
|
|
|
|
|
g_debug_cmd = cmdsave;
|
|
|
|
|
}
|
|
|
|
|
}, T("idlecheck"));
|
2006-09-01 22:59:23 +00:00
|
|
|
|
|
|
|
|
// Schedule ourselves again.
|
|
|
|
|
//
|
|
|
|
|
CLinearTimeAbsolute ltaNow;
|
|
|
|
|
ltaNow.GetUTC();
|
|
|
|
|
CLinearTimeDelta ltd;
|
|
|
|
|
ltd.SetSeconds(mudconf.idle_interval);
|
|
|
|
|
mudstate.idle_counter = ltaNow + ltd;
|
|
|
|
|
scheduler.DeferTask(mudstate.idle_counter, PRIORITY_SYSTEM, dispatch_IdleCheck, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
2015-01-06 04:45:40 +00:00
|
|
|
void dispatch_KeepAlive(void *pUnused, int iUnused)
|
|
|
|
|
{
|
|
|
|
|
UNUSED_PARAMETER(pUnused);
|
|
|
|
|
UNUSED_PARAMETER(iUnused);
|
|
|
|
|
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
run_task_body([]() { send_keepalive_nops(); }, T("keepalive"));
|
2015-01-06 04:45:40 +00:00
|
|
|
|
|
|
|
|
// Schedule ourselves again.
|
|
|
|
|
//
|
|
|
|
|
CLinearTimeAbsolute ltaNow;
|
|
|
|
|
ltaNow.GetUTC();
|
|
|
|
|
CLinearTimeDelta ltd;
|
|
|
|
|
ltd.SetSeconds(mudconf.keepalive_interval);
|
|
|
|
|
mudstate.keepalive_counter = ltaNow + ltd;
|
|
|
|
|
scheduler.DeferTask(mudstate.keepalive_counter, PRIORITY_SYSTEM, dispatch_KeepAlive, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
2006-09-01 22:59:23 +00:00
|
|
|
// Check Events Task routine.
|
|
|
|
|
//
|
|
|
|
|
void dispatch_CheckEvents(void *pUnused, int iUnused)
|
|
|
|
|
{
|
|
|
|
|
UNUSED_PARAMETER(pUnused);
|
|
|
|
|
UNUSED_PARAMETER(iUnused);
|
|
|
|
|
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
run_task_body([]()
|
2006-09-01 22:59:23 +00:00
|
|
|
{
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
if (mudconf.control_flags & CF_EVENTCHECK)
|
|
|
|
|
{
|
|
|
|
|
const UTF8 *cmdsave = g_debug_cmd;
|
|
|
|
|
g_debug_cmd = T("< eventcheck >");
|
|
|
|
|
check_events();
|
|
|
|
|
g_debug_cmd = cmdsave;
|
|
|
|
|
}
|
|
|
|
|
}, T("eventcheck"));
|
2006-09-01 22:59:23 +00:00
|
|
|
|
|
|
|
|
// Schedule ourselves again.
|
|
|
|
|
//
|
|
|
|
|
CLinearTimeAbsolute ltaNow;
|
|
|
|
|
ltaNow.GetUTC();
|
|
|
|
|
CLinearTimeDelta ltd = time_15m;
|
|
|
|
|
mudstate.events_counter = ltaNow + ltd;
|
|
|
|
|
scheduler.DeferTask(mudstate.events_counter, PRIORITY_SYSTEM, dispatch_CheckEvents, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void dispatch_CacheTick(void *pUnused, int iUnused)
|
|
|
|
|
{
|
|
|
|
|
UNUSED_PARAMETER(pUnused);
|
|
|
|
|
UNUSED_PARAMETER(iUnused);
|
|
|
|
|
|
2026-03-09 15:12:06 -06:00
|
|
|
const UTF8 *cmdsave = g_debug_cmd;
|
|
|
|
|
g_debug_cmd = T("< cachetick >");
|
2006-09-01 22:59:23 +00:00
|
|
|
|
|
|
|
|
CLinearTimeDelta ltd = 0;
|
|
|
|
|
if (mudconf.cache_tick_period <= ltd)
|
|
|
|
|
{
|
|
|
|
|
mudconf.cache_tick_period.SetSeconds(1);
|
|
|
|
|
}
|
|
|
|
|
|
fix(engine): keep a recurring system task alive after it throws (#2011)
Follow-up to #2010. That change stopped a throwing task from killing the
game. It did not stop a throwing task from killing itself.
Each of the six recurring system dispatchers re-defers its next run on its
LAST line:
cache_tick(); // throws
// Schedule ourselves again.
scheduler.DeferTask(..., dispatch_CacheTick, ...); // never reached
#2010's barrier lives in CScheduler::RunTasks, above these frames, so it
catches the throw only after unwinding has already skipped the DeferTask.
The game survives and the task is gone permanently.
Measured on merged master with a one-shot throw on the third cache tick at
cache_tick_period 1:
before 3 ticks in 30 seconds, then never again
after 29 ticks
For dispatch_DatabaseDump the same shape means automatic dumps stop
permanently and silently -- a game that survives the exception and then
quietly stops saving is losing the database slowly rather than all at
once, which is not the trade #2009 was meant to buy.
dispatch_FreeListReconstruction, dispatch_IdleCheck, dispatch_KeepAlive
and dispatch_CheckEvents have the same structure.
run_task_body() wraps only each dispatcher's body, so the reschedule tail
stays unconditional. It does not replace #2010's barrier: that one still
covers queued command tasks (Task_ProcessCommand), which is the larger
surface and does not self-reschedule.
make test-smoke 1605/0/1. test-alarm, test-dbt, test-scenario,
test-comsys-handoff pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:47:06 -06:00
|
|
|
run_task_body([]() { cache_tick(); }, T("cachetick"));
|
2006-09-01 22:59:23 +00:00
|
|
|
|
|
|
|
|
// Schedule ourselves again.
|
|
|
|
|
//
|
|
|
|
|
CLinearTimeAbsolute ltaNextTime;
|
|
|
|
|
ltaNextTime.GetUTC();
|
|
|
|
|
ltaNextTime += mudconf.cache_tick_period;
|
|
|
|
|
scheduler.DeferTask(ltaNextTime, PRIORITY_SYSTEM, dispatch_CacheTick, 0, 0);
|
2026-03-09 15:12:06 -06:00
|
|
|
g_debug_cmd = cmdsave;
|
2006-09-01 22:59:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void dispatch_CanRestart(void *pUnused, int iUnused)
|
|
|
|
|
{
|
|
|
|
|
UNUSED_PARAMETER(pUnused);
|
|
|
|
|
UNUSED_PARAMETER(iUnused);
|
|
|
|
|
mudstate.bCanRestart = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void init_timer(void)
|
|
|
|
|
{
|
|
|
|
|
CLinearTimeAbsolute ltaNow;
|
|
|
|
|
ltaNow.GetUTC();
|
|
|
|
|
|
|
|
|
|
// Setup re-occuring Free List Reconstruction task.
|
|
|
|
|
//
|
|
|
|
|
CLinearTimeDelta ltd;
|
|
|
|
|
ltd.SetSeconds((mudconf.check_offset == 0) ? mudconf.check_interval : mudconf.check_offset);
|
|
|
|
|
mudstate.check_counter = ltaNow + ltd;
|
|
|
|
|
scheduler.DeferTask(mudstate.check_counter, PRIORITY_SYSTEM,
|
|
|
|
|
dispatch_FreeListReconstruction, 0, 0);
|
|
|
|
|
|
|
|
|
|
// Setup re-occuring Database Dump task.
|
|
|
|
|
//
|
|
|
|
|
ltd.SetSeconds((mudconf.dump_offset == 0) ? mudconf.dump_interval : mudconf.dump_offset);
|
|
|
|
|
mudstate.dump_counter = ltaNow + ltd;
|
|
|
|
|
scheduler.DeferTask(mudstate.dump_counter, PRIORITY_SYSTEM,
|
|
|
|
|
dispatch_DatabaseDump, 0, 0);
|
|
|
|
|
|
|
|
|
|
// Setup re-occuring Idle Check task.
|
|
|
|
|
//
|
|
|
|
|
ltd.SetSeconds(mudconf.idle_interval);
|
|
|
|
|
mudstate.idle_counter = ltaNow + ltd;
|
|
|
|
|
scheduler.DeferTask(mudstate.idle_counter, PRIORITY_SYSTEM,
|
|
|
|
|
dispatch_IdleCheck, 0, 0);
|
|
|
|
|
|
|
|
|
|
// Setup re-occuring Check Events task.
|
|
|
|
|
//
|
|
|
|
|
mudstate.events_counter = ltaNow + time_15s;
|
|
|
|
|
scheduler.DeferTask(mudstate.events_counter, PRIORITY_SYSTEM,
|
|
|
|
|
dispatch_CheckEvents, 0, 0);
|
|
|
|
|
|
2015-01-06 04:45:40 +00:00
|
|
|
// Setup re-occuring KeepAlive task.
|
|
|
|
|
//
|
|
|
|
|
ltd.SetSeconds(mudconf.keepalive_interval);
|
|
|
|
|
mudstate.keepalive_counter = ltaNow + ltd;
|
|
|
|
|
scheduler.DeferTask(mudstate.keepalive_counter, PRIORITY_SYSTEM, dispatch_KeepAlive, 0, 0);
|
|
|
|
|
|
2006-09-01 22:59:23 +00:00
|
|
|
// Setup re-occuring cache_tick task.
|
|
|
|
|
//
|
|
|
|
|
ltd.SetSeconds(0);
|
|
|
|
|
if (mudconf.cache_tick_period <= ltd)
|
|
|
|
|
{
|
|
|
|
|
mudconf.cache_tick_period.SetSeconds(1);
|
|
|
|
|
}
|
|
|
|
|
scheduler.DeferTask(ltaNow+mudconf.cache_tick_period, PRIORITY_SYSTEM,
|
|
|
|
|
dispatch_CacheTick, 0, 0);
|
|
|
|
|
|
fix(engine): the restart-arm timer is system work, not user work (#2131)
muxscript idled ~15.15 seconds after EOF on every invocation, regardless
of workload — five thousand JIT compilations moved its wall clock by
22 ms while this held it for 15 s. strace showed the shape exactly:
fifteen one-second polls in script_loop's post-EOF arm, waiting for
CScheduler::HasPendingUserTasks to go false.
What kept it true: init_timer schedules dispatch_CanRestart — a one-shot
that arms the @restart throttle 15 seconds after startup — at
PRIORITY_OBJECT, inside the (PRIORITY_SYSTEM, PRIORITY_SUSPEND) band the
exit predicate counts as user work. It is maintenance wearing a user
priority: the predicate's own comment excludes "dumps, idle checks,
keepalives — which recur forever" but had no way to know about a
one-shot safety timer filed in the wrong band.
Moved to PRIORITY_SYSTEM. This also means the throttle arms even while
@disable'd dequeuing blocks the user band — the right behaviour for a
safety timer. (The comment above the call also said "10 seconds" while
the code said 15; now it says what the code does.)
Measured: `think hi` 15.153s -> 0.030s, faster than the @shutdown
workaround (0.134s). The completeness gate is intact: `@wait 3` holds
the process open exactly 3.0s and the deferred task runs before exit.
Every muxscript-driven suite (smoke, growth, codiff, jit parity) drops
the same 15 s per invocation.
Full `make test EXPECT_CONFIG="jit=yes"`: 35 passed, 1 skipped
(stubslave, not configured), 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 07:12:57 -06:00
|
|
|
// Setup one-shot task to enable restarting 15 seconds after startmux.
|
2006-09-01 22:59:23 +00:00
|
|
|
//
|
fix(engine): the restart-arm timer is system work, not user work (#2131)
muxscript idled ~15.15 seconds after EOF on every invocation, regardless
of workload — five thousand JIT compilations moved its wall clock by
22 ms while this held it for 15 s. strace showed the shape exactly:
fifteen one-second polls in script_loop's post-EOF arm, waiting for
CScheduler::HasPendingUserTasks to go false.
What kept it true: init_timer schedules dispatch_CanRestart — a one-shot
that arms the @restart throttle 15 seconds after startup — at
PRIORITY_OBJECT, inside the (PRIORITY_SYSTEM, PRIORITY_SUSPEND) band the
exit predicate counts as user work. It is maintenance wearing a user
priority: the predicate's own comment excludes "dumps, idle checks,
keepalives — which recur forever" but had no way to know about a
one-shot safety timer filed in the wrong band.
Moved to PRIORITY_SYSTEM. This also means the throttle arms even while
@disable'd dequeuing blocks the user band — the right behaviour for a
safety timer. (The comment above the call also said "10 seconds" while
the code said 15; now it says what the code does.)
Measured: `think hi` 15.153s -> 0.030s, faster than the @shutdown
workaround (0.134s). The completeness gate is intact: `@wait 3` holds
the process open exactly 3.0s and the deferred task runs before exit.
Every muxscript-driven suite (smoke, growth, codiff, jit parity) drops
the same 15 s per invocation.
Full `make test EXPECT_CONFIG="jit=yes"`: 35 passed, 1 skipped
(stubslave, not configured), 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 07:12:57 -06:00
|
|
|
// PRIORITY_SYSTEM, deliberately (#2131). This is maintenance — it arms
|
|
|
|
|
// the @restart throttle — but it sat at PRIORITY_OBJECT, inside the band
|
|
|
|
|
// CScheduler::HasPendingUserTasks counts as user work. muxscript's
|
|
|
|
|
// post-EOF exit predicate therefore saw "pending user tasks" until this
|
|
|
|
|
// timer expired, and every scripted invocation idled ~15 seconds doing
|
|
|
|
|
// nothing: five thousand JIT compilations moved muxscript's wall clock
|
|
|
|
|
// by 22 ms while this task held it for 15.15 s. SYSTEM also means the
|
|
|
|
|
// throttle arms even while @disable'd dequeuing blocks the user band,
|
|
|
|
|
// which is the right behaviour for a safety timer.
|
|
|
|
|
//
|
|
|
|
|
scheduler.DeferTask(ltaNow+time_15s, PRIORITY_SYSTEM, dispatch_CanRestart, 0, 0);
|
2006-09-01 22:59:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* ---------------------------------------------------------------------------
|
|
|
|
|
* * do_timewarp: Adjust various internal timers.
|
|
|
|
|
*/
|
|
|
|
|
|
2007-09-02 13:48:10 -07:00
|
|
|
void do_timewarp(dbref executor, dbref caller, dbref enactor, int eval, int key, UTF8 *arg, const UTF8 *cargs[], int ncargs)
|
2006-09-01 22:59:23 +00:00
|
|
|
{
|
2006-09-06 06:17:46 +00:00
|
|
|
UNUSED_PARAMETER(eval);
|
2007-09-02 13:48:10 -07:00
|
|
|
UNUSED_PARAMETER(cargs);
|
|
|
|
|
UNUSED_PARAMETER(ncargs);
|
2006-09-06 06:17:46 +00:00
|
|
|
|
2006-09-01 22:59:23 +00:00
|
|
|
int secs;
|
|
|
|
|
|
fix(win32): migrate the remaining mux_atol callers to mux_atoi64 (#1373)
Completes the sweep the issue called for. mux_atol returns long, which
is 32-bit on LLP64, so every caller silently truncated on Windows. Two
of those were real defects (the truthiness family and cf_size, fixed in
the preceding commits); the rest were latent, waiting for a value large
enough to matter.
Rather than audit 290 sites for whether each can reach 2^31 today, use
the 64-bit parser everywhere and remove the class. A dbref cannot
overflow now, but nothing stops a later caller passing that same site a
timestamp or a byte count.
Pure 1:1 substitution: 285 lines changed, and every removed line
contained mux_atol while every added line contains mux_atoi64. No
control flow, no types, no behaviour beyond the wider parse.
This is a NO-OP on LP64 -- long is already 64-bit on Linux and macOS, so
the generated code there is unchanged. It only widens the parse on
Windows. Narrowing destinations are unaffected either way: `int x =
mux_atoi64(s)` truncates exactly as `int x = mux_atol(s)` did, on both
models.
Left alone: mux_atol itself in mathutil, its declaration, and three
comments that name it. Callers that genuinely want 32-bit semantics can
still ask for them; none appear to.
Verified on Windows: full solution builds clean with no new warnings,
smoke is 1418 passed / 16 failed / 0 crashes / 306 of 306 dispatched --
identical to before the sweep, with the same 16 build-configuration
failures (exp3 module not loaded, hmac/digest behind UNIX_DIGEST).
Spot checks after the change: the boolean family returns 1 for multiples
of 2^32, cf_size round-trips 3000000000 and still reads -1 as unlimited,
and arithmetic, string and list functions are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:03:45 -06:00
|
|
|
secs = mux_atoi64(arg);
|
2006-09-01 22:59:23 +00:00
|
|
|
|
|
|
|
|
// Sem/Wait queues
|
|
|
|
|
//
|
|
|
|
|
if ((key == 0) || (key & TWARP_QUEUE))
|
|
|
|
|
{
|
2018-10-03 17:54:51 +00:00
|
|
|
do_queue(executor, caller, enactor, 0, QUEUE_WARP, arg, nullptr, 0);
|
2006-09-01 22:59:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Once these are adjusted, we need to Cancel and reschedule the task.
|
|
|
|
|
//
|
|
|
|
|
CLinearTimeDelta ltd;
|
|
|
|
|
ltd.SetSeconds(secs);
|
|
|
|
|
if (key & TWARP_DUMP)
|
|
|
|
|
{
|
|
|
|
|
mudstate.dump_counter -= ltd;
|
|
|
|
|
scheduler.CancelTask(dispatch_DatabaseDump, 0, 0);
|
|
|
|
|
scheduler.DeferTask(mudstate.dump_counter, PRIORITY_SYSTEM, dispatch_DatabaseDump, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
if (key & TWARP_CLEAN)
|
|
|
|
|
{
|
|
|
|
|
mudstate.check_counter -= ltd;
|
|
|
|
|
scheduler.CancelTask(dispatch_FreeListReconstruction, 0, 0);
|
|
|
|
|
scheduler.DeferTask(mudstate.check_counter, PRIORITY_SYSTEM, dispatch_FreeListReconstruction, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
if (key & TWARP_IDLE)
|
|
|
|
|
{
|
|
|
|
|
mudstate.idle_counter -= ltd;
|
|
|
|
|
scheduler.CancelTask(dispatch_IdleCheck, 0, 0);
|
|
|
|
|
scheduler.DeferTask(mudstate.idle_counter, PRIORITY_SYSTEM, dispatch_IdleCheck, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
if (key & TWARP_EVENTS)
|
|
|
|
|
{
|
|
|
|
|
mudstate.events_counter -= ltd;
|
|
|
|
|
scheduler.CancelTask(dispatch_CheckEvents, 0, 0);
|
|
|
|
|
scheduler.DeferTask(mudstate.events_counter, PRIORITY_SYSTEM, dispatch_CheckEvents, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-07-31 09:09:18 -06:00
|
|
|
bool CScheduler::DeferTask(const CLinearTimeAbsolute& ltaWhen, int iPriority,
|
2006-09-01 22:59:23 +00:00
|
|
|
FTASK *fpTask, void *arg_voidptr, int arg_Integer)
|
|
|
|
|
{
|
2026-07-31 09:09:18 -06:00
|
|
|
// #1871: nothrow so OOM is a clean false rather than an exception; the
|
|
|
|
|
// previous void path treated both OOM and Insert failure as silent success
|
|
|
|
|
// at wait_que, leaking the BQUE and queue accounting.
|
|
|
|
|
//
|
|
|
|
|
PTASK_RECORD pTask = new (std::nothrow) TASK_RECORD;
|
|
|
|
|
if (!pTask)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2006-09-01 22:59:23 +00:00
|
|
|
|
|
|
|
|
pTask->ltaWhen = ltaWhen;
|
|
|
|
|
pTask->iPriority = iPriority;
|
|
|
|
|
pTask->fpTask = fpTask;
|
|
|
|
|
pTask->arg_voidptr = arg_voidptr;
|
|
|
|
|
pTask->arg_Integer = arg_Integer;
|
|
|
|
|
pTask->m_Ticket = m_Ticket++;
|
|
|
|
|
|
|
|
|
|
// Must add to the WhenHeap so that network is still serviced.
|
|
|
|
|
//
|
2026-03-05 14:10:09 -07:00
|
|
|
if (!m_WhenHeap.Insert(pTask))
|
2007-02-02 17:34:56 +00:00
|
|
|
{
|
|
|
|
|
delete pTask;
|
2026-07-31 09:09:18 -06:00
|
|
|
return false;
|
2007-02-02 17:34:56 +00:00
|
|
|
}
|
2026-07-31 09:09:18 -06:00
|
|
|
return true;
|
2006-09-01 22:59:23 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-31 09:09:18 -06:00
|
|
|
bool CScheduler::DeferImmediateTask(int iPriority, FTASK *fpTask, void *arg_voidptr, int arg_Integer)
|
2006-09-01 22:59:23 +00:00
|
|
|
{
|
2026-07-31 09:09:18 -06:00
|
|
|
PTASK_RECORD pTask = new (std::nothrow) TASK_RECORD;
|
|
|
|
|
if (!pTask)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2006-09-01 22:59:23 +00:00
|
|
|
|
|
|
|
|
//pTask->ltaWhen = ltaWhen;
|
|
|
|
|
pTask->iPriority = iPriority;
|
|
|
|
|
pTask->fpTask = fpTask;
|
|
|
|
|
pTask->arg_voidptr = arg_voidptr;
|
|
|
|
|
pTask->arg_Integer = arg_Integer;
|
|
|
|
|
pTask->m_Ticket = m_Ticket++;
|
|
|
|
|
|
|
|
|
|
// Must add to the WhenHeap so that network is still serviced.
|
|
|
|
|
//
|
2026-03-05 14:10:09 -07:00
|
|
|
if (!m_WhenHeap.Insert(pTask))
|
2007-02-02 17:34:56 +00:00
|
|
|
{
|
|
|
|
|
delete pTask;
|
2026-07-31 09:09:18 -06:00
|
|
|
return false;
|
2007-02-02 17:34:56 +00:00
|
|
|
}
|
2026-07-31 09:09:18 -06:00
|
|
|
return true;
|
2006-09-01 22:59:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CScheduler::CancelTask(FTASK *fpTask, void *arg_voidptr, int arg_Integer)
|
|
|
|
|
{
|
|
|
|
|
m_WhenHeap.CancelTask(fpTask, arg_voidptr, arg_Integer);
|
|
|
|
|
m_PriorityHeap.CancelTask(fpTask, arg_voidptr, arg_Integer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CScheduler::ReadyTasks(const CLinearTimeAbsolute& ltaNow)
|
|
|
|
|
{
|
|
|
|
|
// Move ready-to-run tasks off the WhenHeap and onto the PriorityHeap.
|
|
|
|
|
//
|
|
|
|
|
PTASK_RECORD pTask = m_WhenHeap.PeekAtTopmost();
|
2007-02-02 17:34:56 +00:00
|
|
|
while ( pTask
|
|
|
|
|
&& pTask->ltaWhen < ltaNow)
|
2006-09-01 22:59:23 +00:00
|
|
|
{
|
2026-03-05 14:10:09 -07:00
|
|
|
pTask = m_WhenHeap.RemoveTopmost();
|
2006-09-01 22:59:23 +00:00
|
|
|
if (pTask)
|
|
|
|
|
{
|
2018-10-03 17:54:51 +00:00
|
|
|
if ( nullptr == pTask->fpTask
|
2026-03-05 14:10:09 -07:00
|
|
|
|| !m_PriorityHeap.Insert(pTask))
|
2006-09-01 22:59:23 +00:00
|
|
|
{
|
|
|
|
|
delete pTask;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
pTask = m_WhenHeap.PeekAtTopmost();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int CScheduler::RunTasks(const CLinearTimeAbsolute& ltaNow)
|
|
|
|
|
{
|
|
|
|
|
ReadyTasks(ltaNow);
|
|
|
|
|
if (mudconf.active_q_chunk)
|
|
|
|
|
{
|
|
|
|
|
return RunTasks(mudconf.active_q_chunk);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return RunAllTasks();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int CScheduler::RunTasks(int iCount)
|
|
|
|
|
{
|
|
|
|
|
int nTasks = 0;
|
|
|
|
|
while (iCount--)
|
|
|
|
|
{
|
|
|
|
|
PTASK_RECORD pTask = m_PriorityHeap.PeekAtTopmost();
|
|
|
|
|
if (!pTask) break;
|
|
|
|
|
|
|
|
|
|
if (pTask->iPriority > m_minPriority)
|
|
|
|
|
{
|
|
|
|
|
// This is related to CF_DEQUEUE and also to untimed (SUSPENDED)
|
|
|
|
|
// semaphore entries that we would like to manage together with
|
|
|
|
|
// the timed ones.
|
|
|
|
|
//
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-03-05 14:10:09 -07:00
|
|
|
pTask = m_PriorityHeap.RemoveTopmost();
|
2006-09-01 22:59:23 +00:00
|
|
|
if (pTask)
|
|
|
|
|
{
|
|
|
|
|
if (pTask->fpTask)
|
|
|
|
|
{
|
fix(engine,net): contain exceptions on the command/task path (#2009)
Everything softcode does runs under CScheduler::RunTasks -- command
parsing, function evaluation, mail, comsys, @dump -- and all of it
allocates. There was no catch between that frame and main(), so a
throw became std::terminate -> abort -> SIGABRT, and signals.cpp
handles SIGABRT by logging and exit(1): no dump_restart_db(), no
re-exec. A SIGSEGV on the same line forks, dumps and execl()s a fresh
netmux, so an exception cost the database where a null dereference
would have self-healed.
The issue treated 2.14 reachability as inferred, since its gdb trace
came from 2.13. It is now demonstrated here: with a one-shot
bad_alloc injected into ConnectionBase::close() and a login failed
past retry_limit, unpatched master dies with Abort trap: 6 and refuses
further connections.
Three barriers.
1. Per task, inside CScheduler::RunTasks. Chosen over per-tick
containment for two reasons. One bad command dies without dropping
the rest of the tick. And it keeps the delete reachable: pTask is
already off the priority heap when fpTask runs, so an escaping
throw leaked the record as well as losing the task -- per-tick
containment cannot fix that.
Abandoning one task is survivable. process_command() resets
func_nest_lev, func_invk_ctr, ntfy_nest_lev and lock_nest_lev at
the top of every command, so a half-finished command cannot poison
the next one's limits. It leaks that command's lbufs, which is
bounded and far cheaper than losing the database.
2. A netmux-side backstop around g_pIGameEngine->RunTasks. The
per-task barrier lives in engine.so while run_main_loop is in
netmux -- a module boundary 2.13 does not have, since it reaches
RunTasks directly. The backstop covers a throw from the
scheduler's own frame (the heap operations around the task record)
and anything crossing the boundary whose type the per-task arms did
not match: engine.so is built -fvisibility=hidden, so an
engine-local exception type has no exported typeinfo in netmux and
lands in catch (...).
3. close_contained() for the two conn->close() calls inside the
existing handleNetworkEvent catch arms. They were bare, and a
throw from close() is the demonstrated trigger, so they would have
escaped the handler and aborted anyway. A barrier that can abort
is not a barrier. Abandoning the close leaves the connection to be
reaped by the ordinary idle path, which is survivable.
Verified by injection on macOS arm64, baseline first so that a pass
could not be vacuous -- the close only fires once retries_left reaches
zero (retry_limit defaults to 3), so a single bad connect would have
proved nothing:
unpatched attempt 2: connection closed; Abort trap: 6
DEAD: cannot connect (Errno 61); pid gone
with fix attempt 3: connection closed cleanly
ALIVE: server answered 725 bytes
MUX BUG/TASK : Exception escaped a scheduled task
(std::bad_alloc); task abandoned.
The liveness check is a follow-up connection that must be answered,
not merely accepted; a listening socket outlives a dying process.
That also exercises cross-binary propagation: the throw originates in
libganl, is caught in engine.so, and netmux continues -- under
libc++abi on arm64, a different unwinder and architecture from the
Linux x86-64 run on the 2.13 side.
make test: 33 passed, 1 skipped (stubslave, not configured), 0 failed.
2026-08-03 16:13:54 -06:00
|
|
|
// #2009: exception barrier. Everything softcode does runs
|
|
|
|
|
// under here -- command parsing, function evaluation, mail,
|
|
|
|
|
// comsys, @dump -- and all of it allocates. There is no catch
|
|
|
|
|
// between this frame and main(), so a throw becomes
|
|
|
|
|
// std::terminate -> abort -> SIGABRT, and signals.cpp handles
|
|
|
|
|
// SIGABRT by logging and exit(1): no dump_restart_db(), no
|
|
|
|
|
// re-exec. A SIGSEGV on the same line forks, dumps and
|
|
|
|
|
// execl()s a fresh netmux, so an exception costs the database
|
|
|
|
|
// where a null dereference would have self-healed.
|
|
|
|
|
//
|
|
|
|
|
// Contained per task rather than per tick so that one bad
|
|
|
|
|
// command dies without dropping the rest of the tick. It also
|
|
|
|
|
// keeps the delete below reachable: pTask is already off the
|
|
|
|
|
// heap by this point, so an escaping throw would leak the
|
|
|
|
|
// record in addition to losing the task.
|
|
|
|
|
//
|
|
|
|
|
// Abandoning one task is survivable. process_command() resets
|
|
|
|
|
// func_nest_lev, func_invk_ctr, ntfy_nest_lev and lock_nest_lev
|
|
|
|
|
// at the top of every command, so a half-finished command
|
|
|
|
|
// cannot poison the next one's limits. It does leak that
|
|
|
|
|
// command's lbufs -- bounded, and far cheaper than losing the
|
|
|
|
|
// database.
|
|
|
|
|
//
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
pTask->fpTask(pTask->arg_voidptr, pTask->arg_Integer);
|
|
|
|
|
}
|
|
|
|
|
catch (const std::exception &e)
|
|
|
|
|
{
|
|
|
|
|
STARTLOG(LOG_BUGS, T("BUG"), T("TASK"));
|
|
|
|
|
log_printf(T("Exception escaped a scheduled task (%s); task abandoned."),
|
|
|
|
|
reinterpret_cast<const UTF8 *>(e.what()));
|
|
|
|
|
ENDLOG;
|
|
|
|
|
}
|
|
|
|
|
catch (...)
|
|
|
|
|
{
|
|
|
|
|
STARTLOG(LOG_BUGS, T("BUG"), T("TASK"));
|
|
|
|
|
log_printf(T("Unknown exception escaped a scheduled task; task abandoned."));
|
|
|
|
|
ENDLOG;
|
|
|
|
|
}
|
2006-09-01 22:59:23 +00:00
|
|
|
nTasks++;
|
|
|
|
|
}
|
|
|
|
|
delete pTask;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nTasks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int CScheduler::RunAllTasks(void)
|
|
|
|
|
{
|
|
|
|
|
int nTotalTasks = 0;
|
|
|
|
|
|
|
|
|
|
int nTasks;
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
nTasks = RunTasks(100);
|
|
|
|
|
nTotalTasks += nTasks;
|
|
|
|
|
} while (nTasks);
|
|
|
|
|
|
|
|
|
|
return nTotalTasks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CScheduler::WhenNext(CLinearTimeAbsolute *ltaWhen)
|
|
|
|
|
{
|
|
|
|
|
// Check the Priority Queue first.
|
|
|
|
|
//
|
|
|
|
|
PTASK_RECORD pTask = m_PriorityHeap.PeekAtTopmost();
|
|
|
|
|
if (pTask)
|
|
|
|
|
{
|
|
|
|
|
if (pTask->iPriority <= m_minPriority)
|
|
|
|
|
{
|
|
|
|
|
ltaWhen->SetSeconds(0);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check the When Queue next.
|
|
|
|
|
//
|
|
|
|
|
pTask = m_WhenHeap.PeekAtTopmost();
|
|
|
|
|
if (pTask)
|
|
|
|
|
{
|
|
|
|
|
*ltaWhen = pTask->ltaWhen;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 12:11:13 -05:00
|
|
|
bool CScheduler::HasPendingUserTasks(void)
|
|
|
|
|
{
|
|
|
|
|
// "User work" is any queued or timed task with a priority above system
|
|
|
|
|
// maintenance (dumps, idle checks, keepalives — which recur forever) and
|
|
|
|
|
// below the suspended band (semaphore-parked entries, which never wake
|
|
|
|
|
// without an external notify). A CLI run is finished once only those two
|
|
|
|
|
// classes remain, even though delayed @wait tasks must still be honored.
|
|
|
|
|
//
|
|
|
|
|
return 0 < m_WhenHeap.CountInPriorityRange(PRIORITY_SYSTEM, PRIORITY_SUSPEND)
|
|
|
|
|
|| 0 < m_PriorityHeap.CountInPriorityRange(PRIORITY_SYSTEM, PRIORITY_SUSPEND);
|
|
|
|
|
}
|
|
|
|
|
|
2006-09-01 22:59:23 +00:00
|
|
|
void CScheduler::TraverseUnordered(SCHLOOK *pfLook)
|
|
|
|
|
{
|
2026-03-05 14:10:09 -07:00
|
|
|
if (m_WhenHeap.TraverseUnordered(pfLook))
|
2006-09-01 22:59:23 +00:00
|
|
|
{
|
2026-03-05 14:10:09 -07:00
|
|
|
m_PriorityHeap.TraverseUnordered(pfLook);
|
2006-09-01 22:59:23 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CScheduler::TraverseOrdered(SCHLOOK *pfLook)
|
|
|
|
|
{
|
2026-03-05 14:10:09 -07:00
|
|
|
m_PriorityHeap.TraverseOrdered(pfLook);
|
|
|
|
|
m_WhenHeap.TraverseOrdered(pfLook);
|
2006-09-01 22:59:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CScheduler::SetMinPriority(int arg_minPriority)
|
|
|
|
|
{
|
|
|
|
|
m_minPriority = arg_minPriority;
|
|
|
|
|
}
|
2006-11-05 06:28:50 +00:00
|
|
|
|
|
|
|
|
void CScheduler::Shrink(void)
|
|
|
|
|
{
|
|
|
|
|
m_WhenHeap.Shrink();
|
|
|
|
|
m_PriorityHeap.Shrink();
|
2006-11-05 06:31:31 +00:00
|
|
|
}
|