Skip to content

Commit e51e736

Browse files
committed
fix(database): add deadlock detection and expected long write guard
- Introduce expected_long_write_guard to mark long write operations such as replay and snapshot import, preventing deadlock watchdog misclassification - Add a write-lock deadlock detector in snapshot plugin that tracks write lock hold duration and triggers hard exit on wedged writers exceeding threshold - Define WRITE_LOCK_DEADLOCK_MS constant to 45000ms as the deadlock timeout - Ensure deadlock detector excludes legitimate long writes from exit triggers - Improve logging messages to explain fatal write lock holds and recovery steps
1 parent 01a1a28 commit e51e736

3 files changed

Lines changed: 51 additions & 1 deletion

File tree

libraries/chain/database.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,10 @@ namespace graphene { namespace chain {
668668

669669
ilog("Replaying blocks...");
670670

671+
// Full replay holds the write lock for the whole run; mark it so
672+
// the deadlock watchdog does not mistake it for a wedged writer.
673+
expected_long_write_guard long_write_guard(*this);
674+
671675
uint64_t skip_flags =
672676
skip_block_size_check |
673677
skip_validator_signature |
@@ -788,6 +792,11 @@ namespace graphene { namespace chain {
788792
ilog("Replaying blocks from dlt_block_log (${from}..${to}, ${count} blocks)...",
789793
("from", from_block_num)("to", dlt_last)("count", dlt_last - from_block_num + 1));
790794

795+
// Replaying many blocks legitimately holds the write lock for a
796+
// long time; tell the deadlock watchdog not to mistake it for a
797+
// wedged writer.
798+
expected_long_write_guard long_write_guard(*this);
799+
791800
uint64_t skip_flags =
792801
skip_block_size_check |
793802
skip_validator_signature |

plugins/snapshot/plugin.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include <boost/asio.hpp>
3535
#include <fstream>
3636
#include <cstdio>
37+
#include <cstdlib>
3738
#include <set>
3839
#include <map>
3940
#include <algorithm>
@@ -884,6 +885,14 @@ class snapshot_plugin::plugin_impl {
884885
// Stalled sync detection for DLT mode
885886
bool enable_stalled_sync_detection = false;
886887
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;
887896
fc::time_point last_block_received_time;
888897
std::unique_ptr<fc::thread> stalled_sync_thread; // dedicated thread (main thread can't run fc fibers)
889898
fc::future<void> stalled_sync_check_future;
@@ -933,6 +942,7 @@ constexpr uint64_t snapshot_plugin::plugin_impl::RATE_LIMIT_WINDOW_SEC;
933942
constexpr uint32_t snapshot_plugin::plugin_impl::MAX_CONCURRENT_CONNECTIONS;
934943
constexpr uint32_t snapshot_plugin::plugin_impl::CONNECTION_TIMEOUT_SEC;
935944
constexpr uint32_t snapshot_plugin::plugin_impl::WATCHDOG_CHECK_INTERVAL_SEC;
945+
constexpr uint64_t snapshot_plugin::plugin_impl::WRITE_LOCK_DEADLOCK_MS;
936946

937947
fc::mutable_variant_object snapshot_plugin::plugin_impl::serialize_state() {
938948
fc::mutable_variant_object state;
@@ -1425,6 +1435,9 @@ void snapshot_plugin::plugin_impl::load_snapshot(const fc::path& input_path) {
14251435

14261436
// Import objects in dependency order
14271437
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);
14281441
db.with_strong_write_lock([&]() {
14291442
try {
14301443
// Clear ALL existing multi-instance objects before importing.
@@ -2100,6 +2113,34 @@ void snapshot_plugin::plugin_impl::check_stalled_sync_loop() {
21002113
break;
21012114
}
21022115

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+
21032144
// If a snapshot is currently in progress, the snapshot's
21042145
// strong read lock prevents block processing from acquiring
21052146
// write locks. This is an expected temporary stall, not a

thirdparty/chainbase

0 commit comments

Comments
 (0)