Skip to content

Lock Improvements for [SR-12851] #130

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

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Sources/TSCBasic/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ add_library(TSCBasic
TerminalController.swift
Thread.swift
Tuple.swift
misc.swift)
misc.swift
WeakDictionary.swift)

target_compile_options(TSCBasic PUBLIC
# Don't use GNU strerror_r on Android.
"$<$<PLATFORM_ID:Android>:SHELL:-Xcc -U_GNU_SOURCE>"
Expand Down
36 changes: 36 additions & 0 deletions Sources/TSCBasic/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import TSCLibc
import Foundation
import Dispatch

public enum FileSystemError: Swift.Error {
/// Access to the path is denied.
Expand Down Expand Up @@ -200,6 +201,9 @@ public protocol FileSystem: class {

/// Move a file or directory.
func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws

/// Execute the given block while holding the lock.
func withLock<T>(on path: AbsolutePath, type: LockType, _ body: () throws -> T) throws -> T
}

/// Convenience implementations (default arguments aren't permitted in protocol
Expand Down Expand Up @@ -240,6 +244,10 @@ public extension FileSystem {
func getFileInfo(_ path: AbsolutePath) throws -> FileInfo {
throw FileSystemError.unsupported
}

func withLock<T>(on path: AbsolutePath, type: LockType, _ body: () throws -> T) throws -> T {
throw FileSystemError.unsupported
}
}

/// Concrete FileSystem implementation which communicates with the local file system.
Expand Down Expand Up @@ -450,6 +458,11 @@ private class LocalFileSystem: FileSystem {
guard !exists(destinationPath) else { throw FileSystemError.alreadyExistsAtDestination }
try FileManager.default.moveItem(at: sourcePath.asURL, to: destinationPath.asURL)
}

func withLock<T>(on path: AbsolutePath, type: LockType = .exclusive, _ body: () throws -> T) throws -> T {
let lock = FileLock(name: path.basename, cachePath: path.parentDirectory)
return try lock.withLock(type: type, body)
}
}

// FIXME: This class does not yet support concurrent mutation safely.
Expand Down Expand Up @@ -502,6 +515,10 @@ public class InMemoryFileSystem: FileSystem {

/// The root filesytem.
private var root: Node
/// A map that keeps weak references to all locked files.
private var lockFiles = WeakDictionary<AbsolutePath, DispatchQueue>()
/// Used to access lockFiles in a thread safe manner.
private let lockFilesLock = Lock()

public init() {
root = Node(.directory(DirectoryContents()))
Expand Down Expand Up @@ -760,6 +777,21 @@ public class InMemoryFileSystem: FileSystem {

contents.entries[sourcePath.basename] = nil
}

public func withLock<T>(on path: AbsolutePath, type: LockType = .exclusive, _ body: () throws -> T) throws -> T {
var fileQueue: DispatchQueue

lockFilesLock.lock()
if let queue = lockFiles[path] {
fileQueue = queue
} else {
fileQueue = DispatchQueue(label: "foo", attributes: .concurrent)
lockFiles[path] = fileQueue
}
lockFilesLock.unlock()

return try fileQueue.sync(flags: type == .exclusive ? .barrier : .init() , execute: body)
}
}

/// A rerooted view on an existing FileSystem.
Expand Down Expand Up @@ -864,6 +896,10 @@ public class RerootedFileSystemView: FileSystem {
public func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
try underlyingFileSystem.move(from: formUnderlyingPath(sourcePath), to: formUnderlyingPath(sourcePath))
}

public func withLock<T>(on path: AbsolutePath, type: LockType = .exclusive, _ body: () throws -> T) throws -> T {
return try underlyingFileSystem.withLock(on: formUnderlyingPath(path), type: type, body)
}
}

/// Public access to the local FS proxy.
Expand Down
61 changes: 37 additions & 24 deletions Sources/TSCBasic/Lock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
import Foundation
import TSCLibc

public enum LockType {
case exclusive
case shared
}

/// A simple lock wrapper.
public struct Lock {
private let _lock = NSLock()
Expand All @@ -19,10 +24,18 @@ public struct Lock {
public init() {
}

func lock() {
_lock.lock()
}

func unlock() {
_lock.unlock()
}

/// Execute the given block while holding the lock.
public func withLock<T> (_ body: () throws -> T) rethrows -> T {
_lock.lock()
defer { _lock.unlock() }
lock()
defer { unlock() }
return try body()
}
}
Expand All @@ -37,9 +50,9 @@ enum ProcessLockError: Swift.Error {
public final class FileLock {
/// File descriptor to the lock file.
#if os(Windows)
private var handle: HANDLE?
@ThreadLocal @AutoClosing private var handle: HANDLE?
#else
private var fileDescriptor: CInt?
@ThreadLocal @AutoClosing private var fileDescriptor: CInt?
#endif

/// Path to the lock file.
Expand All @@ -56,14 +69,14 @@ public final class FileLock {
/// Try to aquire a lock. This method will block until lock the already aquired by other process.
///
/// Note: This method can throw if underlying POSIX methods fail.
public func lock() throws {
public func lock(type: LockType = .exclusive) throws {
#if os(Windows)
if handle == nil {
let h = lockFile.pathString.withCString(encodedAs: UTF16.self, {
let h: HANDLE = lockFile.pathString.withCString(encodedAs: UTF16.self, {
CreateFileW(
$0,
UInt32(GENERIC_READ) | UInt32(GENERIC_WRITE),
0,
UInt32(FILE_SHARE_READ) | UInt32(FILE_SHARE_WRITE),
nil,
DWORD(OPEN_ALWAYS),
DWORD(FILE_ATTRIBUTE_NORMAL),
Expand All @@ -79,9 +92,17 @@ public final class FileLock {
overlapped.Offset = 0
overlapped.OffsetHigh = 0
overlapped.hEvent = nil
if !LockFileEx(handle, DWORD(LOCKFILE_EXCLUSIVE_LOCK), 0,
DWORD(INT_MAX), DWORD(INT_MAX), &overlapped) {
throw ProcessLockError.unableToAquireLock(errno: Int32(GetLastError()))
switch type {
case .exclusive:
if !LockFileEx(handle, DWORD(LOCKFILE_EXCLUSIVE_LOCK), 0,
DWORD(INT_MAX), DWORD(INT_MAX), &overlapped) {
throw ProcessLockError.unableToAquireLock(errno: Int32(GetLastError()))
}
case .shared:
if !LockFileEx(handle, 0, 0,
DWORD(INT_MAX), DWORD(INT_MAX), &overlapped) {
throw ProcessLockError.unableToAquireLock(errno: Int32(GetLastError()))
}
}
#else
// Open the lock file.
Expand All @@ -94,7 +115,9 @@ public final class FileLock {
}
// Aquire lock on the file.
while true {
if flock(fileDescriptor!, LOCK_EX) == 0 {
if type == .exclusive && flock(fileDescriptor!, LOCK_EX) == 0 {
break
} else if type == .shared && flock(fileDescriptor!, LOCK_SH) == 0 {
break
}
// Retry if interrupted.
Expand All @@ -113,24 +136,14 @@ public final class FileLock {
overlapped.hEvent = nil
UnlockFileEx(handle, 0, DWORD(INT_MAX), DWORD(INT_MAX), &overlapped)
#else
guard let fd = fileDescriptor else { return }
guard let fd = fileDescriptor else { return }
flock(fd, LOCK_UN)
#endif
}

deinit {
#if os(Windows)
guard let handle = handle else { return }
CloseHandle(handle)
#else
guard let fd = fileDescriptor else { return }
close(fd)
#endif
}

/// Execute the given block while holding the lock.
public func withLock<T>(_ body: () throws -> T) throws -> T {
try lock()
public func withLock<T>(type: LockType = .exclusive, _ body: () throws -> T) throws -> T {
try lock(type: type)
defer { unlock() }
return try body()
}
Expand Down
67 changes: 67 additions & 0 deletions Sources/TSCBasic/Thread.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import Foundation
import TSCLibc

/// This class bridges the gap between Darwin and Linux Foundation Threading API.
/// It provides closure based execution and a join method to block the calling thread
Expand Down Expand Up @@ -64,6 +65,15 @@ final public class Thread {
}
}
}

/// Causes the calling thread to yield execution to another thread.
public static func yield() {
#if os(Windows)
SwitchToThread()
#else
sched_yield()
#endif
}
}

#if canImport(Darwin)
Expand All @@ -85,3 +95,60 @@ final private class ThreadImpl: Foundation.Thread {
// Thread on Linux supports closure so just use it directly.
typealias ThreadImpl = Foundation.Thread
#endif

protocol Defaultable {
static var defaultValue: Self { get }
}

extension Optional: Defaultable {
static var defaultValue: Optional<Wrapped> { .none }
}

/// `ThreadLocal` properties are thread-specific. Every thread has its own instance of the wrapped property.
@propertyWrapper final class ThreadLocal<Value: Defaultable> {
private var storage: NSMutableDictionary { ThreadImpl.current.threadDictionary }
private let key = UUID().uuidString

var wrappedValue: Value {
get {
if let value = storage[key] as? Value {
return value
} else {
let value = Value.defaultValue
storage[key] = value
return value
}
}
set { storage[key] = newValue }
}

init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
}

/// Automatically closes the wrapped file descriptor on deinit.
@propertyWrapper final class AutoClosing: NSObject, Defaultable {
#if os(Windows)
typealias T = HANDLE
#else
typealias T = CInt
#endif
var wrappedValue: T?

static var defaultValue: AutoClosing { AutoClosing(wrappedValue: .none) }

init(wrappedValue: T?) {
self.wrappedValue = wrappedValue
}

deinit {
if let wrappedValue = wrappedValue {
#if os(Windows)
CloseHandle(wrappedValue)
#else
close(wrappedValue)
#endif
}
}
}
28 changes: 28 additions & 0 deletions Sources/TSCBasic/WeakDictionary.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

/// A dictionary that only keeps weak references to its values.
struct WeakDictionary<Key: Hashable, Value: AnyObject> {

private struct WeakReference<Value: AnyObject> {
weak var reference: Value?

init(_ value: Value?) {
self.reference = value
}
}

private var storage = Dictionary<Key, WeakReference<Value>>()

subscript(key: Key) -> Value? {
get { storage[key]?.reference }
set(newValue) { storage[key] = WeakReference(newValue) }
}
}
Loading