Skip to content

Commit 863ee31

Browse files
committed
Fix some BTree concurrency bugs
1 parent 2d64661 commit 863ee31

2 files changed

Lines changed: 67 additions & 11 deletions

File tree

include/fs/BTree.h

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
#include "stdint.h"
2525
#include "stddef.h"
2626
#include "string.h"
27+
#include <mutex>
28+
#include <shared_mutex>
2729

2830

2931
/**
@@ -85,6 +87,7 @@ class BTreeBase {
8587
* @return true if the key is already present, false if the key was not previously present and has been inserted
8688
*/
8789
bool insert(const T& val) {
90+
//std::unique_lock<std::shared_mutex> lock(treeMutex);
8891
btree_node_t<T, N> target;
8992
btree_node_header_t<N>& header = target.header;
9093
uint64_t offset;
@@ -126,6 +129,7 @@ class BTreeBase {
126129
* @return true if the key was already present and overwritten, false if the key was not previously present and has been newly inserted
127130
*/
128131
bool overwrite(const T& val) {
132+
//std::unique_lock<std::shared_mutex> lock(treeMutex);
129133
btree_node_t<T, N> target;
130134
uint64_t offset;
131135

@@ -171,6 +175,7 @@ class BTreeBase {
171175
* @return true if the value was found, false if not
172176
*/
173177
bool find(T& val) {
178+
//std::shared_lock<std::shared_mutex> lock(treeMutex);
174179
btree_node_t<T, N> target;
175180
uint64_t offset;
176181

@@ -190,6 +195,7 @@ class BTreeBase {
190195
* @return true if a match was found, false if not
191196
*/
192197
bool findNext(T& val) {
198+
//std::shared_lock<std::shared_mutex> lock(treeMutex);
193199
btree_node_t<T, N> node = getRootNode();
194200
uint64_t offset = getRootOffset();
195201
bool found = false;
@@ -243,6 +249,7 @@ class BTreeBase {
243249
* @return true if the value was found and removed, false if not
244250
*/
245251
bool remove(T& val) {
252+
//std::unique_lock<std::shared_mutex> lock(treeMutex);
246253
btree_node_t<T, N> node;
247254
uint64_t offset;
248255

@@ -277,6 +284,14 @@ class BTreeBase {
277284
*/
278285
explicit BTreeBase(int (*compare) (const T&, const T&)) : compare(compare) {}
279286

287+
// The mutex is non-copyable/non-movable but instances of BTreeBase are
288+
// embedded in other containers (e.g. TreeMap) that rely on copy/move.
289+
// Each instance gets its own fresh mutex.
290+
BTreeBase(const BTreeBase& other) : compare(other.compare) {}
291+
BTreeBase(BTreeBase&& other) noexcept : compare(other.compare) {}
292+
BTreeBase& operator=(const BTreeBase& other) { compare = other.compare; return *this; }
293+
BTreeBase& operator=(BTreeBase&& other) noexcept { compare = other.compare; return *this; }
294+
280295
/**
281296
* Finds which node is a good candidate for adding the given value. The outputted node will always be at the
282297
* bottom of the tree, unless the value is already present. If the value is present in the tree, then this will
@@ -524,6 +539,8 @@ class BTreeBase {
524539
virtual void overwriteNodeHeader(uint64_t offset, const btree_node_t<T, N>& node) = 0;
525540

526541
int (*compare) (const T&, const T&); /**< Comparison function for elements of type T. */
542+
543+
mutable std::shared_mutex mutex;
527544
};
528545

529546

@@ -575,9 +592,7 @@ class BTree: public BTreeBase<T, N> {
575592
*/
576593
void initialize() {
577594
btree_node_t<T, N> root{{0, 0, 0, -1, 0, {}}, {}};
578-
579-
file.seek(rootOffset);
580-
file.write(root);
595+
file.pwrite(root, rootOffset);
581596
}
582597

583598
protected:
@@ -589,9 +604,7 @@ class BTree: public BTreeBase<T, N> {
589604
*/
590605
btree_node_t<T, N> getNode(uint64_t offset) const override {
591606
btree_node_t<T, N> resultat;
592-
593-
file.seek(offset, SEEK_SET);
594-
file.read(resultat);
607+
file.pread(resultat, (off_t)offset);
595608
return resultat;
596609
}
597610

@@ -618,7 +631,7 @@ class BTree: public BTreeBase<T, N> {
618631
*/
619632
uint64_t addNode(const btree_node_t<T, N>& node) override {
620633
uint64_t offset = file.seekToEndWithPadding(8);
621-
file.write(node);
634+
file.pwrite(node, offset);
622635
return offset;
623636
}
624637

@@ -628,8 +641,7 @@ class BTree: public BTreeBase<T, N> {
628641
* @param node Node data.
629642
*/
630643
void overwriteNode(uint64_t offset, const btree_node_t<T, N>& node) override {
631-
file.seek(offset);
632-
file.write(node);
644+
file.pwrite(node, (off_t)offset);
633645
}
634646

635647
/**
@@ -638,8 +650,7 @@ class BTree: public BTreeBase<T, N> {
638650
* @param node Node data containing the header.
639651
*/
640652
void overwriteNodeHeader(uint64_t offset, const btree_node_t<T, N>& node) override {
641-
file.seek(offset);
642-
file.write(&node, sizeof(node.header));
653+
file.pwrite(&node, sizeof(node.header), (off_t)offset);
643654
}
644655

645656
off_t rootOffset; /**< Offset of the root node in the file. */

src/tests/test_FreeSpaceFile.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818

1919
#include <string>
2020
#include <cstdint>
21+
#include <thread>
22+
#include <vector>
23+
#include <atomic>
2124
#include "universaltime.h"
2225
#include "gtest/gtest.h"
2326
#include "fs/FdHandle.h"
@@ -279,6 +282,48 @@ TEST(FreeSpaceFileTest, ReinsertAfterAllocate) {
279282
if (!HasFailure()) unlink(test_file);
280283
}
281284

285+
// Regression test: BTree is documented as threadsafe, and FreeSpaceFile is
286+
// accessed concurrently (e.g. from a ThreadPool by CatalogFile). Allocating
287+
// and freeing regions from multiple threads must not corrupt the BTree's
288+
// on-disk nodes or crash inside the comparator.
289+
TEST(FreeSpaceFileTest, ConcurrentAllocateAndFree) {
290+
std::string path = makeTempFileName("freespace_concurrent");
291+
const char* test_file = path.c_str();
292+
293+
unlink(test_file);
294+
FdHandle handle = FdHandle::open(test_file, O_RDWR | O_CREAT, 0660);
295+
FreeSpaceFile fs(std::move(handle));
296+
297+
const int numThreads = 8;
298+
const int opsPerThread = 200;
299+
300+
std::atomic<bool> failed(false);
301+
std::vector<std::thread> threads;
302+
threads.reserve(numThreads);
303+
304+
for (int t = 0; t < numThreads; t++) {
305+
threads.emplace_back([&fs, &failed, t, opsPerThread]() {
306+
for (int i = 0; i < opsPerThread; i++) {
307+
uint32_t size = 64 + ((uint32_t)((t * 131) + (i * 17)) & 0x3FF);
308+
off_t off = fs.getFreeRegion(size);
309+
if (off < 0) {
310+
failed.store(true);
311+
return;
312+
}
313+
fs.markFreeRegion(off, size);
314+
}
315+
});
316+
}
317+
318+
for (std::thread& th : threads)
319+
th.join();
320+
321+
EXPECT_FALSE(failed.load());
322+
323+
if (!HasFailure()) unlink(test_file);
324+
}
325+
326+
282327
TEST(FreeSpaceFileTest, NeverAllocatesBeforeHeader) {
283328
std::string path = makeTempFileName("freespace_header");
284329
const char* test_file = path.c_str();

0 commit comments

Comments
 (0)