1212#include < iomanip>
1313#include < random>
1414
15+ #include < execinfo.h> // backtrace() for think() watchdog stack dump
16+
1517// Static members for DensePPLNSWindow precomputed decay table
1618std::vector<uint64_t > btc::DensePPLNSWindow::s_decay_table;
1719uint64_t btc::DensePPLNSWindow::s_decay_per = 0 ;
@@ -394,6 +396,19 @@ void NodeImpl::processing_shares_phase2(HandleSharesData& data, NetService addr)
394396 {
395397 std::unique_lock lock (m_tracker_mutex, std::try_to_lock);
396398 if (!lock.owns_lock ()) {
399+ // ── Backpressure (V36 livelock defense-in-depth) ──────────────
400+ // If think() is wedged/slow the deferred queue could grow without
401+ // bound and blow memory. Cap it: over MAX_PENDING_ADDS we DROP the
402+ // new batch (peers re-advertise their best share, so dropped shares
403+ // are re-requested later) and warn instead of growing unbounded.
404+ if (m_pending_adds.size () >= MAX_PENDING_ADDS ) {
405+ LOG_WARNING << " [ASYNC-DEFER] BACKPRESSURE: pending_adds at cap ("
406+ << m_pending_adds.size () << " /" << MAX_PENDING_ADDS
407+ << " ), dropping batch of " << data.m_items .size ()
408+ << " shares from " << addr.to_string ()
409+ << " — think() may be wedged" ;
410+ return ;
411+ }
397412 LOG_INFO << " [ASYNC-DEFER] processing_shares_phase2: mutex busy, queuing "
398413 << data.m_items .size () << " shares from " << addr.to_string ()
399414 << " (pending=" << m_pending_adds.size () + 1 << " )" ;
@@ -1313,6 +1328,82 @@ void NodeImpl::prune_shares(const uint256& /*best_share*/)
13131328
13141329// (old phases 5-7 removed — replaced by p2pool-style pruning above)
13151330
1331+ // ── think() watchdog (V36 livelock defense-in-depth) ────────────────────
1332+ // Best-effort recovery if a think() cycle wedges. Runs ENTIRELY on the IO
1333+ // thread and NEVER acquires m_tracker_mutex — so it stays responsive even
1334+ // while the compute thread holds the exclusive lock. Uses an atomic deadline
1335+ // + generation counter rather than a per-dispatch timer object lifetime so a
1336+ // late fire cannot act on a newer cycle.
1337+ void NodeImpl::arm_think_watchdog ()
1338+ {
1339+ if (!m_context)
1340+ return ;
1341+ auto deadline = std::chrono::steady_clock::now ()
1342+ + std::chrono::seconds (THINK_WATCHDOG_SECONDS );
1343+ m_think_deadline_ns.store (
1344+ std::chrono::duration_cast<std::chrono::nanoseconds>(
1345+ deadline.time_since_epoch ()).count (),
1346+ std::memory_order_relaxed);
1347+ uint64_t gen = m_think_generation.fetch_add (1 , std::memory_order_relaxed) + 1 ;
1348+
1349+ if (!m_watchdog_timer)
1350+ m_watchdog_timer = std::make_unique<boost::asio::steady_timer>(*m_context);
1351+ m_watchdog_timer->expires_after (std::chrono::seconds (THINK_WATCHDOG_SECONDS ));
1352+ m_watchdog_timer->async_wait ([this , gen](const boost::system::error_code& ec) {
1353+ if (ec) return ; // cancelled by disarm (normal completion)
1354+ // Only act if this is still the cycle we armed for and it has not
1355+ // already completed (deadline cleared to 0 by disarm).
1356+ if (m_think_generation.load (std::memory_order_relaxed) != gen)
1357+ return ;
1358+ int64_t dl = m_think_deadline_ns.load (std::memory_order_relaxed);
1359+ if (dl == 0 )
1360+ return ; // cycle completed in the gap before this fire
1361+ auto now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
1362+ std::chrono::steady_clock::now ().time_since_epoch ()).count ();
1363+ if (now_ns < dl)
1364+ return ; // not actually overdue (spurious) — let next arm handle it
1365+
1366+ // (1) Log + best-effort backtrace dump of the current (IO) thread.
1367+ // We CANNOT safely unwind the compute thread's stack from here, but
1368+ // a backtrace of the watchdog firing plus the timing is enough to
1369+ // confirm the wedge and correlate with the last [ASYNC-THINK] logs.
1370+ LOG_ERROR << " [THINK-WATCHDOG] think() cycle exceeded "
1371+ << THINK_WATCHDOG_SECONDS << " s (gen=" << gen
1372+ << " , pending_adds=" << m_pending_adds.size ()
1373+ << " ) — compute thread appears wedged; recovering pipeline" ;
1374+ {
1375+ void * frames[64 ];
1376+ int n = ::backtrace (frames, 64 );
1377+ char ** syms = ::backtrace_symbols (frames, n);
1378+ if (syms) {
1379+ for (int i = 0 ; i < n; ++i)
1380+ LOG_ERROR << " [THINK-WATCHDOG] bt[" << i << " ] " << syms[i];
1381+ ::free (syms);
1382+ }
1383+ }
1384+
1385+ // (2)+(3) Flag the cycle aborted and reset the running flag so the
1386+ // pipeline recovers. NOTE: this does NOT forcibly unwind the compute
1387+ // thread (unsafe); it lets a fresh think() be scheduled. The stuck
1388+ // think(), if it ever returns, will find m_think_running already false
1389+ // and its IO-phase still runs harmlessly (idempotent drain + reset).
1390+ m_think_deadline_ns.store (0 , std::memory_order_relaxed);
1391+ m_think_running.store (false , std::memory_order_relaxed);
1392+ LOG_WARNING << " [THINK-WATCHDOG] m_think_running reset to false; "
1393+ " a new think() cycle may now be scheduled" ;
1394+ });
1395+ }
1396+
1397+ void NodeImpl::disarm_think_watchdog ()
1398+ {
1399+ // Mark cycle complete: clears the deadline so a racing watchdog fire is a
1400+ // no-op, and cancels the pending timer.
1401+ m_think_deadline_ns.store (0 , std::memory_order_relaxed);
1402+ if (m_watchdog_timer) {
1403+ m_watchdog_timer->cancel ();
1404+ }
1405+ }
1406+
13161407void NodeImpl::run_think ()
13171408{
13181409 // Skip if a think() is already running on the compute thread.
@@ -1327,6 +1418,10 @@ void NodeImpl::run_think()
13271418 << " pending_adds=" << m_pending_adds.size ()
13281419 << " peers=" << m_peers.size ();
13291420
1421+ // Arm the think() watchdog (IO-thread timer; never touches the tracker
1422+ // mutex). Disarmed in the IO-phase below on normal completion.
1423+ arm_think_watchdog ();
1424+
13301425 // Capture block_rel_height fn by value for thread safety
13311426 auto block_rel_height = m_block_rel_height_fn
13321427 ? m_block_rel_height_fn
@@ -1394,7 +1489,12 @@ void NodeImpl::run_think()
13941489 // Publish lock-free snapshot for all IO-thread consumers
13951490 publish_snapshot ();
13961491
1397- needs_continue = m_tracker.m_think_needs_continue ;
1492+ // Continue if EITHER the Phase 2 verification budget OR the V36
1493+ // scoring-walk lock-yield budget was exhausted. Both reuse the same
1494+ // run_think() re-post: lock released here, drain_pending_adds() runs,
1495+ // continuation scheduled below.
1496+ needs_continue = m_tracker.m_think_needs_continue
1497+ || m_tracker.m_think_walk_needs_continue ;
13981498
13991499 // Flush verified hashes to LevelDB while lock is held
14001500 flush_verified_to_leveldb ();
@@ -1482,6 +1582,10 @@ void NodeImpl::run_think()
14821582 << " pending batches, peers=" << m_peers.size ();
14831583 drain_pending_adds ();
14841584
1585+ // Cycle completed normally — disarm the watchdog first so it
1586+ // cannot fire on this (now-finished) generation.
1587+ disarm_think_watchdog ();
1588+
14851589 // Clear flag AFTER drain — prevents new think() from starting
14861590 // while deferred shares are still being added to the tracker.
14871591 m_think_running.store (false );
@@ -1494,9 +1598,11 @@ void NodeImpl::run_think()
14941598 }
14951599 } catch (const std::exception& e) {
14961600 LOG_ERROR << " run_think() IO phase failed: " << e.what ();
1601+ disarm_think_watchdog ();
14971602 m_think_running.store (false );
14981603 } catch (...) {
14991604 LOG_ERROR << " run_think() IO phase failed: unknown error" ;
1605+ disarm_think_watchdog ();
15001606 m_think_running.store (false );
15011607 }
15021608 });
0 commit comments