Skip to content

Commit 8c593c4

Browse files
authored
Merge pull request #107 from akyrtzi/mdb_cursor_put_crash
[Database] Make the database path unique to the specific lmdb wrapper instance
2 parents cb810c3 + 8f77f8c commit 8c593c4

File tree

7 files changed

+99
-35
lines changed

7 files changed

+99
-35
lines changed

Tests/INPUTS/SingleUnit/main.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
void test() {}

Tests/INPUTS/SingleUnit/project.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"sources": ["main.c"]}

Tests/IndexStoreDBTests/IndexStoreDBTests.swift

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ import ISDBTibs
1515
import XCTest
1616
import Foundation
1717

18+
let isTSanEnabled: Bool = {
19+
if let value = ProcessInfo.processInfo.environment["INDEXSTOREDB_ENABLED_THREAD_SANITIZER"] {
20+
return value == "1" || value == "YES"
21+
}
22+
return false
23+
}()
24+
1825
func checkThrows(_ expected: IndexStoreDBError, file: StaticString = #file, line: UInt = #line, _ body: () throws -> ()) {
1926
do {
2027
try body()
@@ -72,7 +79,40 @@ final class IndexStoreDBTests: XCTestCase {
7279
}
7380
}
7481

75-
static var allTests = [
76-
("testErrors", testErrors),
77-
]
82+
func testSymlinkedDBPaths() throws {
83+
// FIXME: This test seems to trigger a false-positive in TSan.
84+
try XCTSkipIf(isTSanEnabled, "skipping because TSan is enabled")
85+
86+
let toolchain = TibsToolchain.testDefault
87+
let libIndexStore = try! IndexStoreLibrary(dylibPath: toolchain.libIndexStore.path)
88+
89+
// This tests against a crash that was manifesting when creating separate `IndexStoreDB` instances against 2 DB paths
90+
// that resolve to the same underlying directory (e.g. they were symlinked).
91+
// It runs a number of iterations though it was not guaranteed that the issue would hit within a specific number
92+
// of iterations, only that at *some* point, if you let it run indefinitely, it could trigger.
93+
let iterations = 100
94+
95+
let indexStorePath: String
96+
do {
97+
// Don't care about specific index data, just want an index store directory containing *something*.
98+
guard let ws = try staticTibsTestWorkspace(name: "SingleUnit") else { return }
99+
try ws.buildAndIndex()
100+
indexStorePath = ws.builder.indexstore.path
101+
}
102+
103+
let fileMgr = FileManager.default
104+
let mainDBPath = tmp + "/db"
105+
let symlinkDBPath1 = tmp + "/db-link1"
106+
let symlinkDBPath2 = tmp + "/db-link2"
107+
try fileMgr.createDirectory(atPath: mainDBPath, withIntermediateDirectories: true, attributes: nil)
108+
try fileMgr.createSymbolicLink(atPath: symlinkDBPath1, withDestinationPath: mainDBPath)
109+
try fileMgr.createSymbolicLink(atPath: symlinkDBPath2, withDestinationPath: mainDBPath)
110+
111+
for _ in 0..<iterations {
112+
DispatchQueue.concurrentPerform(iterations: 2) { idx in
113+
let dbPath = idx == 0 ? symlinkDBPath1 : symlinkDBPath2
114+
let idxDB = try! IndexStoreDB(storePath: indexStorePath, databasePath: dbPath, library: libIndexStore, waitUntilDoneInitializing: true)
115+
}
116+
}
117+
}
78118
}

Tests/IndexStoreDBTests/XCTestManifests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ extension IndexStoreDBTests {
88
static let __allTests__IndexStoreDBTests = [
99
("testCreateIndexStoreAndDBDirs", testCreateIndexStoreAndDBDirs),
1010
("testErrors", testErrors),
11+
("testSymlinkedDBPaths", testSymlinkedDBPaths),
1112
]
1213
}
1314

Utilities/build-script-helper.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ def handle_invocation(swift_exec, args):
6262
if args.sanitize and 'undefined' in args.sanitize:
6363
supp = os.path.join(args.package_path, 'Utilities', 'ubsan_supressions.supp')
6464
env['UBSAN_OPTIONS'] = 'halt_on_error=true,suppressions=%s' % supp
65+
if args.sanitize and 'thread' in args.sanitize:
66+
env['INDEXSTOREDB_ENABLED_THREAD_SANITIZER'] = '1'
6567

6668
# Workaround for incremental build bug in swiftpm.
6769
print('Cleaning ' + args.build_path)

lib/Database/Database.cpp

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -71,29 +71,38 @@ static int filesForProvider_compare(const MDB_val *a, const MDB_val *b) {
7171
return IDCode::compare(lhs->UnitCode, rhs->UnitCode);
7272
}
7373

74+
/// Returns a global serial queue for stale database removal.
75+
static dispatch_queue_t getDiscardedDBsCleanupQueue() {
76+
static dispatch_queue_t queue;
77+
static dispatch_once_t onceToken = 0;
78+
dispatch_once(&onceToken, ^{
79+
dispatch_queue_attr_t qosAttribute = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_BACKGROUND, 0);
80+
queue = dispatch_queue_create("indexstoredb.db.discarded_dbs_cleanup", qosAttribute);
81+
});
82+
return queue;
83+
}
84+
7485
Database::Implementation::Implementation() {
7586
ReadTxnGroup = dispatch_group_create();
7687
TxnSyncQueue = dispatch_queue_create("indexstoredb.db.txn_sync", DISPATCH_QUEUE_CONCURRENT);
77-
dispatch_queue_attr_t qosAttribute = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_BACKGROUND, 0);
78-
DiscardedDBsCleanupQueue = dispatch_queue_create("indexstoredb.db.discarded_dbs_cleanup", qosAttribute);
7988
}
8089
Database::Implementation::~Implementation() {
8190
if (!IsReadOnly) {
8291
DBEnv.close();
92+
assert(!SavedPath.empty() && !UniquePath.empty());
8393
// In case some other process already created the 'saved' path, override it so
8494
// that the 'last one wins'.
85-
llvm::sys::fs::rename(SavedPath, llvm::Twine(ProcessPath)+"saved"+DeadProcessDBSuffix);
86-
if (std::error_code ec = llvm::sys::fs::rename(ProcessPath, SavedPath)) {
95+
llvm::sys::fs::rename(SavedPath, llvm::Twine(UniquePath)+"-saved"+DeadProcessDBSuffix);
96+
if (std::error_code ec = llvm::sys::fs::rename(UniquePath, SavedPath)) {
8797
// If the database directory already got removed or some other process beat
8898
// us during the tiny window between the above 2 renames, then give-up,
8999
// and let the database to be discarded.
90-
LOG_WARN_FUNC("failed moving process directory to 'saved': " << ec.message());
100+
LOG_WARN_FUNC("failed moving " << llvm::sys::path::filename(UniquePath) << " directory to 'saved': " << ec.message());
91101
}
92102
}
93103

94104
dispatch_release(ReadTxnGroup);
95105
dispatch_release(TxnSyncQueue);
96-
dispatch_release(DiscardedDBsCleanupQueue);
97106
}
98107

99108
std::shared_ptr<Database::Implementation>
@@ -105,12 +114,14 @@ Database::Implementation::create(StringRef path, bool readonly, Optional<size_t>
105114

106115
SmallString<128> savedPathBuf = versionPath;
107116
llvm::sys::path::append(savedPathBuf, "saved");
108-
SmallString<128> processPathBuf = versionPath;
117+
SmallString<128> prefixPathBuf = versionPath;
109118
#if defined(WIN32)
110-
llvm::raw_svector_ostream(processPathBuf) << "/p" << GetCurrentProcess();
119+
llvm::raw_svector_ostream(prefixPathBuf) << "/p" << GetCurrentProcess();
111120
#else
112-
llvm::raw_svector_ostream(processPathBuf) << "/p" << getpid();
121+
llvm::raw_svector_ostream(prefixPathBuf) << "/p" << getpid();
113122
#endif
123+
llvm::raw_svector_ostream(prefixPathBuf) << "-";
124+
SmallString<128> uniqueDirPath;
114125

115126
bool existingDB = true;
116127

@@ -121,30 +132,33 @@ Database::Implementation::create(StringRef path, bool readonly, Optional<size_t>
121132
}
122133
return false;
123134
};
135+
auto createUniqueDirOrError = [&error, &prefixPathBuf, &uniqueDirPath]() {
136+
uniqueDirPath.clear();
137+
if (std::error_code ec = llvm::sys::fs::createUniqueDirectory(prefixPathBuf, uniqueDirPath)) {
138+
llvm::raw_string_ostream(error) << "failed creating directory '" << uniqueDirPath << "': " << ec.message();
139+
return true;
140+
}
141+
return false;
142+
};
124143

125144
StringRef dbPath;
126145
if (!readonly) {
127146
if (createDirectoriesOrError(versionPath))
128147
return nullptr;
129148

130-
// Move the currently stored database to a process-specific directory.
131-
// When the database closes it moves the process-specific directory back to
149+
// Move the currently stored database to a unique directory to isolate it.
150+
// When the database closes it moves the unique directory back to
132151
// the '/saved' one. If we crash before closing, then we'll discard the database
133-
// that is left in the process-specific one.
134-
135-
// In case some other process that had the same pid crashed and left the database
136-
// directory, move it aside and we'll clear it later.
137-
// We are already protected from opening the same database twice from the same
138-
// process via getLMDBDatabaseRefForPath().
139-
llvm::sys::fs::rename(processPathBuf, llvm::Twine(processPathBuf)+DeadProcessDBSuffix);
152+
// that is left in the unique directory that includes the process pid number.
153+
if (createUniqueDirOrError())
154+
return nullptr;
140155

141-
if (llvm::sys::fs::rename(savedPathBuf, processPathBuf)) {
142-
// No existing database, create a new directory.
156+
// This succeeds for moving to an empty directory, like the newly constructed `uniqueDirPath`.
157+
if (llvm::sys::fs::rename(savedPathBuf, uniqueDirPath)) {
158+
// No existing database, just use the new directory.
143159
existingDB = false;
144-
if (createDirectoriesOrError(processPathBuf))
145-
return nullptr;
146160
}
147-
dbPath = processPathBuf;
161+
dbPath = uniqueDirPath;
148162
} else {
149163
dbPath = savedPathBuf;
150164
}
@@ -155,7 +169,7 @@ Database::Implementation::create(StringRef path, bool readonly, Optional<size_t>
155169
db->IsReadOnly = readonly;
156170
db->VersionedPath = versionPath.str();
157171
db->SavedPath = savedPathBuf.str();
158-
db->ProcessPath = processPathBuf.str();
172+
db->UniquePath = uniqueDirPath.str();
159173
db->DBEnv = lmdb::env::create();
160174
db->DBEnv.set_max_dbs(14);
161175

@@ -215,8 +229,8 @@ Database::Implementation::create(StringRef path, bool readonly, Optional<size_t>
215229
"corrupted database saved at '" << corruptedPathBuf << "'\n"
216230
"creating new database...");
217231

218-
// Recreate the process path for the next attempt.
219-
if (createDirectoriesOrError(processPathBuf))
232+
// Recreate the unique database path for the next attempt.
233+
if (!readonly && createUniqueDirOrError())
220234
return nullptr;
221235
existingDB = false;
222236
goto retry;
@@ -300,7 +314,7 @@ static void cleanupDiscardedDBsImpl(StringRef versionedPath) {
300314

301315
// Finds database subdirectories that are considered dead and removes them.
302316
// A directory is dead if it has been marked with the suffix "-dead" or if it
303-
// has the name "p<PID>" where process PID is no longer running.
317+
// has the name "p<PID>-*" where process PID is no longer running.
304318

305319
#if defined(WIN32)
306320
indexstorePid_t currPID = GetCurrentProcess();
@@ -321,16 +335,22 @@ static void cleanupDiscardedDBsImpl(StringRef versionedPath) {
321335
return true;
322336
if (!path.startswith("p"))
323337
return false;
338+
StringRef pidStr = path.substr(1);
339+
size_t dashIdx = pidStr.find('-');
340+
if (dashIdx == StringRef::npos)
341+
return false;
342+
pidStr = pidStr.substr(0, dashIdx);
324343
size_t pathPID;
325-
if (path.substr(1).getAsInteger(10, pathPID))
344+
if (pidStr.getAsInteger(10, pathPID))
326345
return false;
327346
if ((indexstorePid_t)pathPID == currPID)
328347
return false;
329348
return !isProcessStillExecuting((indexstorePid_t)pathPID);
330349
};
331350

332351
if (shouldRemove(currPath, currPID)) {
333-
remove_directories(currPath);
352+
// FIXME: With `IgnoreErrors` set to true it can hit an assertion if an error occurs.
353+
remove_directories(currPath, /*IgnoreErrors=*/false);
334354
}
335355

336356
Begin.increment(EC);
@@ -339,7 +359,7 @@ static void cleanupDiscardedDBsImpl(StringRef versionedPath) {
339359

340360
void Database::Implementation::cleanupDiscardedDBs() {
341361
std::string localVersionedPath = VersionedPath;
342-
dispatch_async(DiscardedDBsCleanupQueue, ^{
362+
dispatch_async(getDiscardedDBsCleanupQueue(), ^{
343363
cleanupDiscardedDBsImpl(localVersionedPath);
344364
});
345365
}

lib/Database/DatabaseImpl.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,11 @@ class Database::Implementation {
4444

4545
dispatch_group_t ReadTxnGroup;
4646
dispatch_queue_t TxnSyncQueue;
47-
dispatch_queue_t DiscardedDBsCleanupQueue;
4847

4948
bool IsReadOnly;
5049
std::string VersionedPath;
5150
std::string SavedPath;
52-
std::string ProcessPath;
51+
std::string UniquePath;
5352

5453
public:
5554
static std::shared_ptr<Implementation> create(StringRef dbPath, bool readonly, Optional<size_t> initialDBSize, std::string &error);

0 commit comments

Comments
 (0)