tinymux/mux/modules/engine/timer.cpp
Stephen Dennis c023eadc85 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

593 lines
18 KiB
C++

/*! \file timer.cpp
* \brief Mini-task scheduler for timed events.
*
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
#include <new>
#include <exception>
CScheduler scheduler;
// 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;
}
}
// Free List Reconstruction Task routine.
//
void dispatch_FreeListReconstruction(void *pUnused, int iUnused)
{
UNUSED_PARAMETER(pUnused);
UNUSED_PARAMETER(iUnused);
run_task_body([]()
{
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"));
// 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;
run_task_body([&nNextTimeInSeconds]()
{
if (mudconf.control_flags & CF_CHECKPOINT)
{
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
#endif // HAVE_WORKING_FORK
{
fork_and_dump(0);
}
g_debug_cmd = cmdsave;
}
}, T("dump"));
// 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);
run_task_body([]()
{
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"));
// 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);
}
void dispatch_KeepAlive(void *pUnused, int iUnused)
{
UNUSED_PARAMETER(pUnused);
UNUSED_PARAMETER(iUnused);
run_task_body([]() { send_keepalive_nops(); }, T("keepalive"));
// 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);
}
// Check Events Task routine.
//
void dispatch_CheckEvents(void *pUnused, int iUnused)
{
UNUSED_PARAMETER(pUnused);
UNUSED_PARAMETER(iUnused);
run_task_body([]()
{
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"));
// 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);
const UTF8 *cmdsave = g_debug_cmd;
g_debug_cmd = T("< cachetick >");
CLinearTimeDelta ltd = 0;
if (mudconf.cache_tick_period <= ltd)
{
mudconf.cache_tick_period.SetSeconds(1);
}
run_task_body([]() { cache_tick(); }, T("cachetick"));
// Schedule ourselves again.
//
CLinearTimeAbsolute ltaNextTime;
ltaNextTime.GetUTC();
ltaNextTime += mudconf.cache_tick_period;
scheduler.DeferTask(ltaNextTime, PRIORITY_SYSTEM, dispatch_CacheTick, 0, 0);
g_debug_cmd = cmdsave;
}
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);
// 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);
// 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);
// Setup one-shot task to enable restarting 15 seconds after startmux.
//
// 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);
}
/*
* ---------------------------------------------------------------------------
* * do_timewarp: Adjust various internal timers.
*/
void do_timewarp(dbref executor, dbref caller, dbref enactor, int eval, int key, UTF8 *arg, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int secs;
secs = mux_atoi64(arg);
// Sem/Wait queues
//
if ((key == 0) || (key & TWARP_QUEUE))
{
do_queue(executor, caller, enactor, 0, QUEUE_WARP, arg, nullptr, 0);
}
// 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);
}
}
bool CScheduler::DeferTask(const CLinearTimeAbsolute& ltaWhen, int iPriority,
FTASK *fpTask, void *arg_voidptr, int arg_Integer)
{
// #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;
}
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.
//
if (!m_WhenHeap.Insert(pTask))
{
delete pTask;
return false;
}
return true;
}
bool CScheduler::DeferImmediateTask(int iPriority, FTASK *fpTask, void *arg_voidptr, int arg_Integer)
{
PTASK_RECORD pTask = new (std::nothrow) TASK_RECORD;
if (!pTask)
{
return false;
}
//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.
//
if (!m_WhenHeap.Insert(pTask))
{
delete pTask;
return false;
}
return true;
}
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();
while ( pTask
&& pTask->ltaWhen < ltaNow)
{
pTask = m_WhenHeap.RemoveTopmost();
if (pTask)
{
if ( nullptr == pTask->fpTask
|| !m_PriorityHeap.Insert(pTask))
{
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;
}
pTask = m_PriorityHeap.RemoveTopmost();
if (pTask)
{
if (pTask->fpTask)
{
// #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;
}
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;
}
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);
}
void CScheduler::TraverseUnordered(SCHLOOK *pfLook)
{
if (m_WhenHeap.TraverseUnordered(pfLook))
{
m_PriorityHeap.TraverseUnordered(pfLook);
}
}
void CScheduler::TraverseOrdered(SCHLOOK *pfLook)
{
m_PriorityHeap.TraverseOrdered(pfLook);
m_WhenHeap.TraverseOrdered(pfLook);
}
void CScheduler::SetMinPriority(int arg_minPriority)
{
m_minPriority = arg_minPriority;
}
void CScheduler::Shrink(void)
{
m_WhenHeap.Shrink();
m_PriorityHeap.Shrink();
}