|
34 | 34 | #include <boost/asio.hpp> |
35 | 35 | #include <fstream> |
36 | 36 | #include <cstdio> |
| 37 | +#include <cstdlib> |
37 | 38 | #include <set> |
38 | 39 | #include <map> |
39 | 40 | #include <algorithm> |
@@ -884,6 +885,14 @@ class snapshot_plugin::plugin_impl { |
884 | 885 | // Stalled sync detection for DLT mode |
885 | 886 | bool enable_stalled_sync_detection = false; |
886 | 887 | uint32_t stalled_sync_timeout_minutes = 5; |
| 888 | + |
| 889 | + // Write-lock deadlock detector threshold. If a single writer holds the |
| 890 | + // database write lock continuously for longer than this — and no expected |
| 891 | + // long write (snapshot import, reindex/replay) is in progress — the node |
| 892 | + // is wedged and we hard-exit for a runit restart. Well above any healthy |
| 893 | + // push_block / fork-switch (sub-second to a few seconds), well below the |
| 894 | + // minutes a wedge would otherwise spin. |
| 895 | + static constexpr uint64_t WRITE_LOCK_DEADLOCK_MS = 45000; |
887 | 896 | fc::time_point last_block_received_time; |
888 | 897 | std::unique_ptr<fc::thread> stalled_sync_thread; // dedicated thread (main thread can't run fc fibers) |
889 | 898 | fc::future<void> stalled_sync_check_future; |
@@ -933,6 +942,7 @@ constexpr uint64_t snapshot_plugin::plugin_impl::RATE_LIMIT_WINDOW_SEC; |
933 | 942 | constexpr uint32_t snapshot_plugin::plugin_impl::MAX_CONCURRENT_CONNECTIONS; |
934 | 943 | constexpr uint32_t snapshot_plugin::plugin_impl::CONNECTION_TIMEOUT_SEC; |
935 | 944 | constexpr uint32_t snapshot_plugin::plugin_impl::WATCHDOG_CHECK_INTERVAL_SEC; |
| 945 | +constexpr uint64_t snapshot_plugin::plugin_impl::WRITE_LOCK_DEADLOCK_MS; |
936 | 946 |
|
937 | 947 | fc::mutable_variant_object snapshot_plugin::plugin_impl::serialize_state() { |
938 | 948 | fc::mutable_variant_object state; |
@@ -1425,6 +1435,9 @@ void snapshot_plugin::plugin_impl::load_snapshot(const fc::path& input_path) { |
1425 | 1435 |
|
1426 | 1436 | // Import objects in dependency order |
1427 | 1437 | std::cerr << " Importing state into database...\n"; |
| 1438 | + // Importing a large state holds the write lock for a long time; tell the |
| 1439 | + // deadlock watchdog not to mistake it for a wedged writer. |
| 1440 | + chainbase::database::expected_long_write_guard long_write_guard(db); |
1428 | 1441 | db.with_strong_write_lock([&]() { |
1429 | 1442 | try { |
1430 | 1443 | // Clear ALL existing multi-instance objects before importing. |
@@ -2100,6 +2113,34 @@ void snapshot_plugin::plugin_impl::check_stalled_sync_loop() { |
2100 | 2113 | break; |
2101 | 2114 | } |
2102 | 2115 |
|
| 2116 | + // === Write-lock deadlock detector === |
| 2117 | + // A wedged writer holds the database write lock forever — e.g. a |
| 2118 | + // push_block fork-switch whose chainbase undo() failed and was |
| 2119 | + // suppressed (see chainbase ~session()). Every reader then times |
| 2120 | + // out ("Unable to acquire READ lock") and the node spams locks |
| 2121 | + // until it is killed by hand. No in-process recovery can take the |
| 2122 | + // lock back (close/reopen needs that very lock), so the only fix is |
| 2123 | + // to exit and let runit restart vizd — snapshot recovery runs on |
| 2124 | + // startup. expected_long_write() excludes legitimately long writes |
| 2125 | + // (snapshot import, reindex/replay), so this fires only on a true |
| 2126 | + // wedge: no healthy push_block holds the lock anywhere near this |
| 2127 | + // long. This thread never touches the lock, so it stays alive to |
| 2128 | + // observe and act while every DB-touching thread is starved. |
| 2129 | + { |
| 2130 | + uint64_t held_ms = db.write_lock_held_ms(); |
| 2131 | + if (held_ms > WRITE_LOCK_DEADLOCK_MS && !db.expected_long_write()) { |
| 2132 | + std::cerr << "FATAL: database write lock held for " << held_ms |
| 2133 | + << "ms with no expected long write in progress — " |
| 2134 | + << "deadlocked writer (likely a stuck push_block " |
| 2135 | + << "fork-switch). Exiting so runit restarts vizd and " |
| 2136 | + << "recovers from snapshot." << std::endl; |
| 2137 | + std::cerr.flush(); |
| 2138 | + // Hard exit: a graceful shutdown would re-block on the dead |
| 2139 | + // lock (flush/validator_guard need it) and hang again. |
| 2140 | + std::_Exit(1); |
| 2141 | + } |
| 2142 | + } |
| 2143 | + |
2103 | 2144 | // If a snapshot is currently in progress, the snapshot's |
2104 | 2145 | // strong read lock prevents block processing from acquiring |
2105 | 2146 | // write locks. This is an expected temporary stall, not a |
|
0 commit comments