-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Update FileLock API #2036
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update FileLock API #2036
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,26 +31,55 @@ enum ProcessLockError: Swift.Error { | |
case unableToAquireLock(errno: Int32) | ||
} | ||
|
||
/// Provides functionality to aquire a lock on a file via POSIX's flock() method. | ||
/// Provides functionality to aquire an exclusive lock on a file via POSIX's flock() method. | ||
/// It can be used for things like serializing concurrent mutations on a shared resource | ||
/// by mutiple instances of a process. The `FileLock` is not thread-safe. | ||
public final class FileLock { | ||
private typealias FileDescriptor = CInt | ||
|
||
/// File descriptor to the lock file. | ||
#if os(Windows) | ||
private var handle: HANDLE? | ||
#else | ||
private var fileDescriptor: CInt? | ||
private var fileDescriptor: FileDescriptor? | ||
#endif | ||
|
||
/// Path to the lock file. | ||
private let lockFile: AbsolutePath | ||
public let path: AbsolutePath | ||
|
||
/// Create an instance of process lock with a name and the path where | ||
/// the lock file can be created. | ||
/// | ||
/// Note: The cache path should be a valid directory. | ||
public init(name: String, cachePath: AbsolutePath) { | ||
self.lockFile = cachePath.appending(component: name + ".lock") | ||
public init(name: String, in directory: AbsolutePath) { | ||
self.path = directory.appending(component: name) | ||
} | ||
|
||
/// Attempts to acquire a lock and immediately returns a Boolean value | ||
/// that indicates whether the attempt was successful. | ||
/// | ||
/// - Returns: `true` if the lock was acquired, otherwise `false`. | ||
public func `try`() -> Bool { | ||
// Open the lock file. | ||
if self.fileDescriptor == nil, let fileDescriptor = try? openFile(at: path) { | ||
self.fileDescriptor = fileDescriptor | ||
} | ||
|
||
guard let fileDescriptor = self.fileDescriptor else { return false } | ||
|
||
// Aquire lock on the file. | ||
while flock(fileDescriptor, LOCK_EX | LOCK_NB) != 0 { | ||
switch errno { | ||
case EWOULDBLOCK: // non-blocking lock not available | ||
return false | ||
case EINTR: // Retry if interrupted. | ||
continue | ||
default: | ||
return false | ||
} | ||
} | ||
|
||
return true | ||
} | ||
|
||
/// Try to aquire a lock. This method will block until lock the already aquired by other process. | ||
|
@@ -85,19 +114,22 @@ public final class FileLock { | |
#else | ||
// Open the lock file. | ||
if fileDescriptor == nil { | ||
let fd = SPMLibc.open(lockFile.pathString, O_WRONLY | O_CREAT | O_CLOEXEC, 0o666) | ||
let fd = SPMLibc.open(path.pathString, O_WRONLY | O_CREAT | O_CLOEXEC, 0o666) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you update this to use the new |
||
if fd == -1 { | ||
throw FileSystemError(errno: errno) | ||
} | ||
self.fileDescriptor = fd | ||
} | ||
|
||
// Aquire lock on the file. | ||
while true { | ||
if flock(fileDescriptor!, LOCK_EX) == 0 { | ||
break | ||
} | ||
// Retry if interrupted. | ||
if errno == EINTR { continue } | ||
if errno == EINTR { | ||
continue | ||
} | ||
throw ProcessLockError.unableToAquireLock(errno: errno) | ||
} | ||
#endif | ||
|
@@ -129,8 +161,17 @@ public final class FileLock { | |
|
||
/// Execute the given block while holding the lock. | ||
public func withLock<T>(_ body: () throws -> T) throws -> T { | ||
try lock() | ||
defer { unlock() } | ||
try self.lock() | ||
defer { self.unlock() } | ||
return try body() | ||
} | ||
|
||
private func openFile(at path: AbsolutePath) throws -> FileDescriptor { | ||
// Open the lock file. | ||
let fd = SPMLibc.open(path.pathString, O_WRONLY | O_CREAT | O_CLOEXEC, 0o666) | ||
if fd == -1 { | ||
throw FileSystemError(errno: errno) | ||
} | ||
return fd | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,12 +39,34 @@ class LockTests: XCTestCase { | |
let N = 10 | ||
let threads = (1...N).map { idx in | ||
return Thread { | ||
_ = try! SwiftPMProduct.TestSupportExecutable.execute(["fileLockTest", tempDir.path.pathString, sharedResource.path.pathString, String(idx)]) | ||
_ = try! SwiftPMProduct.TestSupportExecutable.execute(["fileLockTest", "test.lock", tempDir.path.pathString, sharedResource.path.pathString, String(idx)]) | ||
} | ||
} | ||
threads.forEach { $0.start() } | ||
threads.forEach { $0.join() } | ||
|
||
XCTAssertEqual(try localFileSystem.readFileContents(sharedResource.path).description, String((N * (N + 1) / 2 ))) | ||
} | ||
|
||
func testFileLockTry() throws { | ||
let tempDir = try TemporaryDirectory(removeTreeOnDeinit: true) | ||
let fileLock = FileLock(name: "test.lock", in: tempDir.path) | ||
|
||
let lockThread = Thread { | ||
try! SwiftPMProduct.TestSupportExecutable.execute(["fileLockLock", "test.lock", tempDir.path.pathString, "3"]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given the trivial implementation, I don't think it's worth adding a test case that will sleep for 3 seconds. It seems fine to land this PR without a test for the new |
||
} | ||
|
||
lockThread.start() | ||
|
||
// wait for the fileLockLock process to launch and acquire a lock | ||
XCTAssertTrue(waitForFile(tempDir.path.appending(component: "test.lock"))) | ||
// try lock | ||
XCTAssertFalse(fileLock.try()) | ||
// wait for lock | ||
try fileLock.lock() | ||
fileLock.unlock() | ||
XCTAssertTrue(fileLock.try()) | ||
|
||
lockThread.join() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't compile on windows so should guard it with
#if os()
checks.