#include #include #include #include #include #include #include #include #include namespace smo { namespace device { namespace { constexpr unsigned int reattachInFlightStaleThresholdMultiplier = 4; } // namespace DeviceReattacher::DeviceReattacher( DeviceManager& parent, std::shared_ptr ioThread) : parent(parent), ioThread(ioThread), timer(ioThread->getIoContext()) { /** EXPLANATION: * The thread on which DeviceReattacher runs is whichever thread executes * the io_context that owns deadline_timer. Timer async_wait handlers * (onTimeout, holdReattachCReq, reattachKnownListCReq) are dispatched on * that thread. ioThread selects that io_context here; start() only arms * the timer on it. */ } mrntt::MrnttNonViralNonPostingInvoker DeviceReattacher::reattachKnownListCReq( [[maybe_unused]] std::exception_ptr &exceptionPtr, [[maybe_unused]] std::function callback) { /** EXPLANATION: * Non-posting: invoked from holdReattachCReq on the timer callback thread * (see ctor). Nested DeviceManager attach APIs still post to MRNTT as * needed via their own viral posting invokers. */ co_await parent.attachAllUnattachedDevicesFromKnownListCReq(); co_return; } void DeviceReattacher::start() { deviceReattacherCanceler.startAcceptingWork(); scheduleNextTimeout(); } void DeviceReattacher::stop() { { sscl::SpinLock::Guard guard(deviceReattacherCanceler.s.lock); reattachOpInFlight = false; deviceReattacherCanceler.s.rsrc.shouldContinue = false; /** EXPLANATION: * Do not call reattachCReqInvoker.reset() here. Forcibly destroying * the invoker would tear down an in-flight reattach coroutine frame * mid-operation. During normal program teardown the optional (and * its invoker) are destroyed with the rest of the binary anyway; leave * a running reattach time to finish if shutdown races with it. */ } timer.cancel(); } void DeviceReattacher::scheduleNextTimeout() { if (deviceReattacherCanceler.isCancellationRequestedUnlocked()) { return; } // Schedule the next timeout using the configured period timer.expires_from_now( boost::posix_time::milliseconds( CONFIG_MRNTT_DEVMGR_REATTACHER_PERIOD_MS)); timer.async_wait( std::bind(&DeviceReattacher::onTimeout, this, std::placeholders::_1)); } void DeviceReattacher::holdReattachCReq() { reattachOpInFlight = true; lastReattachReqTimestamp = std::chrono::steady_clock::now(); reattachCReqInvoker.reset(); reattachCReqInvoker.emplace(reattachKnownListCReq( reattachLifetimeExceptionPtr, [this]() { sscl::co::NonViralCompletion nvc(reattachLifetimeExceptionPtr); if (nvc.hasException()) { try { nvc.checkAndRethrowException(); } catch (const std::exception &e) { std::cerr << "DeviceReattacher: " << e.what() << std::endl; } } sscl::SpinLock::Guard guard(deviceReattacherCanceler.s.lock); reattachOpInFlight = false; })); } void DeviceReattacher::onTimeout(const boost::system::error_code& error) { // Timer was cancelled, which is expected when stopping if (error == boost::asio::error::operation_aborted) { return; } if (error) { std::cerr << "DeviceReattacher: Timer error: " << error.message() << std::endl; return; } sscl::SpinLock::Guard guard(deviceReattacherCanceler.s.lock); if (deviceReattacherCanceler.isCancellationRequestedUnlocked()) { return; } const auto staleThreshold = std::chrono::milliseconds( reattachInFlightStaleThresholdMultiplier * CONFIG_MRNTT_DEVMGR_REATTACHER_PERIOD_MS); // Attempt to reattach all unattached devices from the known list if (!reattachOpInFlight) { holdReattachCReq(); } else { const auto elapsedSinceLastReattachReq = std::chrono::steady_clock::now() - lastReattachReqTimestamp; if (elapsedSinceLastReattachReq >= staleThreshold) { std::cerr << "DeviceReattacher: Reattach op still in flight after " << std::chrono::duration_cast( elapsedSinceLastReattachReq).count() << "ms (threshold " << staleThreshold.count() << "ms); forcing a new reattach request." << std::endl; holdReattachCReq(); } } // Schedule the next timeout scheduleNextTimeout(); } } // namespace device } // namespace smo