init: Take lock on blocks directory in BlockManager ctor #31860

pull TheCharlatan wants to merge 3 commits into bitcoin:master from TheCharlatan:blockmanLock changing 7 files +102 −19
  1. TheCharlatan commented at 8:49 pm on February 13, 2025: contributor

    This ensures consistent directory locking for users of the kernel library by disallowing mistakenly creating multiple BlockManagers that might write into the same block or undo files.

    The change here makes LockDirectory more strict: It now returns an error even if it is called again on the same directory from the same process. However from the description in #12422 , where this feature was introduced, it is not immediately clear what its usage was at the time. Supporting multiple wallets in the same directory is mentioned there, but it is not said why that might be a desirable feature.

  2. DrahtBot commented at 8:49 pm on February 13, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/31860.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK stickies-v

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #31144 (optimization: batch XOR operations 12% faster IBD by l0rinc)
    • #29641 (scripted-diff: Use LogInfo over LogPrintf [WIP, NOMERGE, DRAFT] by maflcko)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  3. in src/node/blockstorage.h:130 in 436df349f0 outdated
    126@@ -127,6 +127,14 @@ struct BlockfileCursor {
    127 
    128 std::ostream& operator<<(std::ostream& os, const BlockfileCursor& cursor);
    129 
    130+class BlockDirLock
    


    stickies-v commented at 2:33 pm on February 14, 2025:
    It seems like this class could easily be reused for chainstate, datadir, wallet, … directories in future commits. Perhaps a class DirectoryLock would make sense?
  4. in src/node/blockstorage.h:134 in 436df349f0 outdated
    126@@ -127,6 +127,14 @@ struct BlockfileCursor {
    127 
    128 std::ostream& operator<<(std::ostream& os, const BlockfileCursor& cursor);
    129 
    130+class BlockDirLock
    131+{
    132+    fs::path m_path;
    133+
    134+public:
    


    stickies-v commented at 3:55 pm on February 14, 2025:
    Copy ctor and assignment operator should probably be deleted?

    stickies-v commented at 6:06 pm on February 14, 2025:
    And have BlockManager::m_lock be a std::unique_ptr<BlockDirLock> instead?

    TheCharlatan commented at 9:36 pm on February 14, 2025:
    I’ve been thinking about allocating it on the heap instead, but I kind of like that declaring it as the first field forces us to initialize it first, which a pointer type would not necessarily. Then again, moving the lock as a unique pointer from the options to the actual class would also probably be easier than defining a move constructor the directory lock class.
  5. in src/init.cpp:1114 in 436df349f0 outdated
    1108@@ -1109,8 +1109,9 @@ static bool LockDirectory(const fs::path& dir, bool probeOnly)
    1109 }
    1110 static bool LockDirectories(bool probeOnly)
    1111 {
    1112-    return LockDirectory(gArgs.GetDataDirNet(), probeOnly) && \
    1113-           LockDirectory(gArgs.GetBlocksDirPath(), probeOnly);
    1114+    // Only allow probing the blocks directory, the BlockManager takes the lock internally.
    1115+    return LockDirectory(gArgs.GetDataDirNet(), probeOnly) &&
    1116+           (probeOnly ? LockDirectory(gArgs.GetBlocksDirPath(), probeOnly) : true);
    


    stickies-v commented at 5:16 pm on February 14, 2025:

    At this point, I don’t see the benefit of this function anymore. Let’s just inline it?

     0diff --git a/src/init.cpp b/src/init.cpp
     1index 579669712e..3a984488af 100644
     2--- a/src/init.cpp
     3+++ b/src/init.cpp
     4@@ -1107,12 +1107,6 @@ static bool LockDirectory(const fs::path& dir, bool probeOnly)
     5     } // no default case, so the compiler can warn about missing cases
     6     assert(false);
     7 }
     8-static bool LockDirectories(bool probeOnly)
     9-{
    10-    // Only allow probing the blocks directory, the BlockManager takes the lock internally.
    11-    return LockDirectory(gArgs.GetDataDirNet(), probeOnly) &&
    12-           (probeOnly ? LockDirectory(gArgs.GetBlocksDirPath(), probeOnly) : true);
    13-}
    14 
    15 bool AppInitSanityChecks(const kernel::Context& kernel)
    16 {
    17@@ -1130,7 +1124,8 @@ bool AppInitSanityChecks(const kernel::Context& kernel)
    18     // Probe the directory locks to give an early error message, if possible
    19     // We cannot hold the directory locks here, as the forking for daemon() hasn't yet happened,
    20     // and a fork will cause weird behavior to them.
    21-    return LockDirectories(true);
    22+    return LockDirectory(gArgs.GetDataDirNet(), /*probeOnly=*/true)
    23+        && LockDirectory(gArgs.GetBlocksDirPath(), /*probeOnly=*/true);
    24 }
    25 
    26 bool AppInitLockDirectories()
    27@@ -1138,11 +1133,8 @@ bool AppInitLockDirectories()
    28     // After daemonization get the directory locks again and hold on to them until exit
    29     // This creates a slight window for a race condition to happen, however this condition is harmless: it
    30     // will at most make us exit without printing a message to console.
    31-    if (!LockDirectories(false)) {
    32-        // Detailed error printed inside LockDirectory
    33-        return false;
    34-    }
    35-    return true;
    36+    // Detailed error printed inside LockDirectory
    37+    return LockDirectory(gArgs.GetDataDirNet(), /*probeOnly=*/false);
    38 }
    39 
    40 bool AppInitInterfaces(NodeContext& node)
    
  6. stickies-v commented at 6:03 pm on February 14, 2025: contributor

    Concept ACK on:

    • making LockDirectory more strict, it confused me recently that I was able to acquire a lock multiple times (within the same process)
    • making BlockManager manage the lock instead of init

    As per https://github.com/TheCharlatan/bitcoin/pull/25, I would be a fan of going further and making the BlockManagerOptions ctor take a BlockDirLock (or, as per my comment a DirectoryLock) to allow more granular error feedback, but this is already a step in the right direction regardless.

    In the future, this approach would also make it more straightforward to e.g. let BlockManager give other threads read-only access to the blocksdir via something like:

     0class DirectoryLock {
     1private:
     2    std::shared_mutex m_mutex;
     3public:
     4    const fs::path m_path;
     5
     6    DirectoryLock(const fs::path& path)
     7        : m_path(path)
     8    {
     9        assert(util::LockDirectory(m_path, ".lock", false) == util::LockResult::Success);
    10    }
    11    ~DirectoryLock() { UnlockDirectory(m_path, ".lock"); }
    12
    13    DirectoryLock(const DirectoryLock&) = delete;
    14    DirectoryLock& operator=(const DirectoryLock&) = delete;
    15
    16    std::unique_lock<std::shared_mutex> lock_exclusive()
    17    {
    18        return std::unique_lock<std::shared_mutex>(m_mutex);
    19    }
    20    std::shared_lock<std::shared_mutex> lock_shared()
    21    {
    22        return std::shared_lock<std::shared_mutex>(m_mutex);
    23    }
    24};
    
  7. util: Prevent multiple LockDirectory calls within the same process
    Previously LockDirectory only prevented concurrent locks across
    different processes, but allowed the same process to re-lock on the same
    directory.
    
    This change is not immediately relevant for its current use, where the
    lock is only supposed to protect against a different process writing on
    the same resources.
    
    This change is relevant for future use by the kernel library, where
    users of the library might mistakenly create multiple instances of an
    object that seek to write to a common resource.
    de844c79d4
  8. TheCharlatan force-pushed on Feb 14, 2025
  9. TheCharlatan force-pushed on Feb 14, 2025
  10. DrahtBot commented at 9:31 pm on February 14, 2025: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/37256718772

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  11. DrahtBot added the label CI failed on Feb 14, 2025
  12. DrahtBot removed the label CI failed on Feb 15, 2025
  13. TheCharlatan force-pushed on Feb 16, 2025
  14. util: Add RAII directory lock
    This makes it easier for a class or a struct to own a lock on a
    directory for the duration of its lifetime. It is used in the next
    commit.
    21300478d9
  15. init: Take lock on blocks directory in BlockManager ctor
    This moves the responsibility of taking the lock for the blocks
    directory into the BlockManager. Use the DirectoryLock wrapper to ensure
    it is the first resource to be acquired and is released again after use.
    
    This is relevant for the kernel library where the lock should be taken
    even if the user fails to explicitly do so.
    51436f85a2
  16. TheCharlatan force-pushed on Feb 16, 2025
  17. TheCharlatan commented at 8:32 am on February 17, 2025: contributor

    I would be a fan of going further and making the BlockManagerOptions ctor take a BlockDirLock (or, as per my comment a DirectoryLock) to allow more granular error feedback, but this is already a step in the right direction regardless.

    I attempted implementing this, but giving the options the lock starts to introduce non-trivial additional semantics to the options: Since the lock needs to be moved from the options to the block manager, the options cannot be re-used. I’m not sure how to implement that and make it clear to the user what is going on.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-02-22 06:12 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me