Skip to content

Commit 2cfba96

Browse files
[FileSystem] Allow exclusive file lock (#114098)
Add parameter to file lock API to allow exclusive file lock. Both Unix and Windows support lock the file exclusively for write for one process and LLVM OnDiskCAS uses exclusive file lock to coordinate CAS creation.
1 parent 0499d3a commit 2cfba96

File tree

4 files changed

+124
-10
lines changed

4 files changed

+124
-10
lines changed

llvm/include/llvm/Support/FileSystem.h

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,6 +1171,12 @@ LLVM_ABI Expected<file_t>
11711171
openNativeFileForRead(const Twine &Name, OpenFlags Flags = OF_None,
11721172
SmallVectorImpl<char> *RealPath = nullptr);
11731173

1174+
/// An enumeration for the lock kind.
1175+
enum class LockKind {
1176+
Exclusive, // Exclusive/writer lock
1177+
Shared // Shared/reader lock
1178+
};
1179+
11741180
/// Try to locks the file during the specified time.
11751181
///
11761182
/// This function implements advisory locking on entire file. If it returns
@@ -1184,6 +1190,7 @@ openNativeFileForRead(const Twine &Name, OpenFlags Flags = OF_None,
11841190
/// @param Timeout Time in milliseconds that the process should wait before
11851191
/// reporting lock failure. Zero value means try to get lock only
11861192
/// once.
1193+
/// @param Kind The kind of the lock used (exclusive/shared).
11871194
/// @returns errc::success if lock is successfully obtained,
11881195
/// errc::no_lock_available if the file cannot be locked, or platform-specific
11891196
/// error_code otherwise.
@@ -1194,12 +1201,15 @@ openNativeFileForRead(const Twine &Name, OpenFlags Flags = OF_None,
11941201
/// descriptor.
11951202
LLVM_ABI std::error_code
11961203
tryLockFile(int FD,
1197-
std::chrono::milliseconds Timeout = std::chrono::milliseconds(0));
1204+
std::chrono::milliseconds Timeout = std::chrono::milliseconds(0),
1205+
LockKind Kind = LockKind::Exclusive);
11981206

11991207
/// Lock the file.
12001208
///
12011209
/// This function acts as @ref tryLockFile but it waits infinitely.
1202-
LLVM_ABI std::error_code lockFile(int FD);
1210+
/// \param FD file descriptor to use for locking.
1211+
/// \param Kind of lock to used (exclusive/shared).
1212+
LLVM_ABI std::error_code lockFile(int FD, LockKind Kind = LockKind::Exclusive);
12031213

12041214
/// Unlock the file.
12051215
///

llvm/lib/Support/Unix/Path.inc

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,13 +1230,21 @@ Expected<size_t> readNativeFileSlice(file_t FD, MutableArrayRef<char> Buf,
12301230
return NumRead;
12311231
}
12321232

1233-
std::error_code tryLockFile(int FD, std::chrono::milliseconds Timeout) {
1233+
std::error_code tryLockFile(int FD, std::chrono::milliseconds Timeout,
1234+
LockKind Kind) {
12341235
auto Start = std::chrono::steady_clock::now();
12351236
auto End = Start + Timeout;
12361237
do {
12371238
struct flock Lock;
12381239
memset(&Lock, 0, sizeof(Lock));
1239-
Lock.l_type = F_WRLCK;
1240+
switch (Kind) {
1241+
case LockKind::Exclusive:
1242+
Lock.l_type = F_WRLCK;
1243+
break;
1244+
case LockKind::Shared:
1245+
Lock.l_type = F_RDLCK;
1246+
break;
1247+
}
12401248
Lock.l_whence = SEEK_SET;
12411249
Lock.l_start = 0;
12421250
Lock.l_len = 0;
@@ -1245,15 +1253,24 @@ std::error_code tryLockFile(int FD, std::chrono::milliseconds Timeout) {
12451253
int Error = errno;
12461254
if (Error != EACCES && Error != EAGAIN)
12471255
return std::error_code(Error, std::generic_category());
1256+
if (Timeout.count() == 0)
1257+
break;
12481258
usleep(1000);
12491259
} while (std::chrono::steady_clock::now() < End);
12501260
return make_error_code(errc::no_lock_available);
12511261
}
12521262

1253-
std::error_code lockFile(int FD) {
1263+
std::error_code lockFile(int FD, LockKind Kind) {
12541264
struct flock Lock;
12551265
memset(&Lock, 0, sizeof(Lock));
1256-
Lock.l_type = F_WRLCK;
1266+
switch (Kind) {
1267+
case LockKind::Exclusive:
1268+
Lock.l_type = F_WRLCK;
1269+
break;
1270+
case LockKind::Shared:
1271+
Lock.l_type = F_RDLCK;
1272+
break;
1273+
}
12571274
Lock.l_whence = SEEK_SET;
12581275
Lock.l_start = 0;
12591276
Lock.l_len = 0;

llvm/lib/Support/Windows/Path.inc

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,8 +1337,10 @@ Expected<size_t> readNativeFileSlice(file_t FileHandle,
13371337
return readNativeFileImpl(FileHandle, Buf, &Overlapped);
13381338
}
13391339

1340-
std::error_code tryLockFile(int FD, std::chrono::milliseconds Timeout) {
1341-
DWORD Flags = LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY;
1340+
std::error_code tryLockFile(int FD, std::chrono::milliseconds Timeout,
1341+
LockKind Kind) {
1342+
DWORD Flags = Kind == LockKind::Exclusive ? LOCKFILE_EXCLUSIVE_LOCK : 0;
1343+
Flags |= LOCKFILE_FAIL_IMMEDIATELY;
13421344
OVERLAPPED OV = {};
13431345
file_t File = convertFDToNativeFile(FD);
13441346
auto Start = std::chrono::steady_clock::now();
@@ -1348,6 +1350,8 @@ std::error_code tryLockFile(int FD, std::chrono::milliseconds Timeout) {
13481350
return std::error_code();
13491351
DWORD Error = ::GetLastError();
13501352
if (Error == ERROR_LOCK_VIOLATION) {
1353+
if (Timeout.count() == 0)
1354+
break;
13511355
::Sleep(1);
13521356
continue;
13531357
}
@@ -1356,8 +1360,8 @@ std::error_code tryLockFile(int FD, std::chrono::milliseconds Timeout) {
13561360
return mapWindowsError(ERROR_LOCK_VIOLATION);
13571361
}
13581362

1359-
std::error_code lockFile(int FD) {
1360-
DWORD Flags = LOCKFILE_EXCLUSIVE_LOCK;
1363+
std::error_code lockFile(int FD, LockKind Kind) {
1364+
DWORD Flags = Kind == LockKind::Exclusive ? LOCKFILE_EXCLUSIVE_LOCK : 0;
13611365
OVERLAPPED OV = {};
13621366
file_t File = convertFDToNativeFile(FD);
13631367
if (::LockFileEx(File, Flags, 0, MAXDWORD, MAXDWORD, &OV))

llvm/unittests/Support/ProgramTest.cpp

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "llvm/Config/llvm-config.h"
1111
#include "llvm/Support/CommandLine.h"
1212
#include "llvm/Support/ConvertUTF.h"
13+
#include "llvm/Support/ExponentialBackoff.h"
1314
#include "llvm/Support/FileSystem.h"
1415
#include "llvm/Support/Path.h"
1516
#include "llvm/Support/Signals.h"
@@ -573,6 +574,88 @@ TEST_F(ProgramEnvTest, TestLockFile) {
573574
sys::fs::remove(LockedFile);
574575
}
575576

577+
TEST_F(ProgramEnvTest, TestLockFileExclusive) {
578+
using namespace llvm::sys;
579+
using namespace std::chrono_literals;
580+
581+
if (const char *LockedFile = getenv("LLVM_PROGRAM_TEST_LOCKED_FILE")) {
582+
// Child process.
583+
int FD2;
584+
ASSERT_NO_ERROR(fs::openFileForReadWrite(LockedFile, FD2,
585+
fs::CD_OpenExisting, fs::OF_None));
586+
587+
// File should currently be non-exclusive locked by the main process, thus
588+
// trying to acquire exclusive lock will fail and trying to acquire
589+
// non-exclusive will succeed.
590+
EXPECT_TRUE(
591+
fs::tryLockFile(FD2, std::chrono::seconds(0), fs::LockKind::Exclusive));
592+
593+
EXPECT_FALSE(
594+
fs::tryLockFile(FD2, std::chrono::seconds(0), fs::LockKind::Shared));
595+
596+
close(FD2);
597+
// Write a file to indicate just finished.
598+
std::string FinishFile = std::string(LockedFile) + "-finished";
599+
int FD3;
600+
ASSERT_NO_ERROR(fs::openFileForReadWrite(FinishFile, FD3, fs::CD_CreateNew,
601+
fs::OF_None));
602+
close(FD3);
603+
exit(0);
604+
}
605+
606+
// Create file that will be locked.
607+
SmallString<64> LockedFile;
608+
int FD1;
609+
ASSERT_NO_ERROR(
610+
fs::createUniqueDirectory("TestLockFileExclusive", LockedFile));
611+
sys::path::append(LockedFile, "file");
612+
ASSERT_NO_ERROR(
613+
fs::openFileForReadWrite(LockedFile, FD1, fs::CD_CreateNew, fs::OF_None));
614+
615+
std::string Executable =
616+
sys::fs::getMainExecutable(TestMainArgv0, &ProgramTestStringArg1);
617+
StringRef argv[] = {Executable,
618+
"--gtest_filter=ProgramEnvTest.TestLockFileExclusive"};
619+
620+
// Add LLVM_PROGRAM_TEST_LOCKED_FILE to the environment of the child.
621+
std::string EnvVar = "LLVM_PROGRAM_TEST_LOCKED_FILE=";
622+
EnvVar += LockedFile.str();
623+
addEnvVar(EnvVar);
624+
625+
// Lock the file.
626+
ASSERT_NO_ERROR(
627+
fs::tryLockFile(FD1, std::chrono::seconds(0), fs::LockKind::Exclusive));
628+
629+
std::string Error;
630+
bool ExecutionFailed;
631+
ProcessInfo PI2 = ExecuteNoWait(Executable, argv, getEnviron(), {}, 0, &Error,
632+
&ExecutionFailed);
633+
ASSERT_FALSE(ExecutionFailed) << Error;
634+
ASSERT_TRUE(Error.empty());
635+
ASSERT_NE(PI2.Pid, ProcessInfo::InvalidPid) << "Invalid process id";
636+
637+
std::string FinishFile = std::string(LockedFile) + "-finished";
638+
// Wait till child process writes the file to indicate the job finished.
639+
bool Finished = false;
640+
ExponentialBackoff Backoff(5s); // timeout 5s.
641+
do {
642+
if (fs::exists(FinishFile)) {
643+
Finished = true;
644+
break;
645+
}
646+
} while (Backoff.waitForNextAttempt());
647+
648+
ASSERT_TRUE(Finished);
649+
ASSERT_NO_ERROR(fs::unlockFile(FD1));
650+
ProcessInfo WaitResult = llvm::sys::Wait(PI2, /*SecondsToWait=*/1, &Error);
651+
ASSERT_TRUE(Error.empty());
652+
ASSERT_EQ(0, WaitResult.ReturnCode);
653+
ASSERT_EQ(WaitResult.Pid, PI2.Pid);
654+
sys::fs::remove(LockedFile);
655+
sys::fs::remove(FinishFile);
656+
sys::fs::remove_directories(sys::path::parent_path(LockedFile));
657+
}
658+
576659
TEST_F(ProgramEnvTest, TestExecuteWithNoStacktraceHandler) {
577660
using namespace llvm::sys;
578661

0 commit comments

Comments
 (0)