/*! \file timer.cpp * \brief Mini-task scheduler for timed events. * */ #include "copyright.h" #include "autoconf.h" #include "config.h" #include "externs.h" #include #include 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 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(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(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(); }