Skip to content

Commit

Permalink
Swift 6 Compatibility (#12)
Browse files Browse the repository at this point in the history
# Fix Swift Concurrency

## ♻️ Current situation & Problem
Updates the flag name.


## ⚙️ Release Notes 
* Update swift concurrency flag name.


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
Supereg authored Jul 12, 2024
1 parent 3326fea commit 4781d96
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 55 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ jobs:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziFoundation
artifactname: SpeziFoundation.xcresult
resultBundle: SpeziFoundation.xcresult
packageios_latest:
name: Build and Test Swift Package iOS Latest
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziFoundation
xcodeversion: latest
swiftVersion: 6
artifactname: SpeziFoundation-Latest.xcresult
resultBundle: SpeziFoundation-Latest.xcresult
packagewatchos:
name: Build and Test Swift Package watchOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import PackageDescription


#if swift(<6)
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("SwiftConcurrency")
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency")
#else
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("SwiftConcurrency")
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency")
#endif


Expand Down
3 changes: 2 additions & 1 deletion Sources/SpeziFoundation/Misc/TimeoutError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ extension TimeoutError: LocalizedError {
/// - Parameters:
/// - timeout: The duration of the timeout.
/// - action: The action to run once the timeout passed.
public func withTimeout(of timeout: Duration, perform action: () async -> Void) async {
@inlinable
public func withTimeout(of timeout: Duration, perform action: @Sendable () async -> Void) async {
try? await Task.sleep(for: timeout)
guard !Task.isCancelled else {
return
Expand Down
93 changes: 46 additions & 47 deletions Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import Foundation
/// ```
///
/// - Warning: `cancelAll` will trigger a runtime error if it attempts to cancel tasks that are not cancellable.
public final class AsyncSemaphore: @unchecked Sendable {
public final class AsyncSemaphore: Sendable {
private enum Suspension {
case cancelable(UnsafeContinuation<Void, Error>)
case regular(UnsafeContinuation<Void, Never>)
Expand All @@ -72,9 +72,9 @@ public final class AsyncSemaphore: @unchecked Sendable {
}


private var value: Int
private var suspendedTasks: [SuspendedTask] = []
private let nsLock = NSLock()
private nonisolated(unsafe) var value: Int

Check warning on line 75 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View workflow job for this annotation

GitHub Actions / CodeQL / Test using xcodebuild or run fastlane

stored property 'value' of 'Sendable'-conforming class 'AsyncSemaphore' is mutable

Check warning on line 75 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View workflow job for this annotation

GitHub Actions / CodeQL / Test using xcodebuild or run fastlane

stored property 'value' of 'Sendable'-conforming class 'AsyncSemaphore' is mutable

Check warning on line 75 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

stored property 'value' of 'Sendable'-conforming class 'AsyncSemaphore' is mutable

Check warning on line 75 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

stored property 'value' of 'Sendable'-conforming class 'AsyncSemaphore' is mutable

Check warning on line 75 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package watchOS / Test using xcodebuild or run fastlane

stored property 'value' of 'Sendable'-conforming class 'AsyncSemaphore' is mutable

Check warning on line 75 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package watchOS / Test using xcodebuild or run fastlane

stored property 'value' of 'Sendable'-conforming class 'AsyncSemaphore' is mutable
private nonisolated(unsafe) var suspendedTasks: [SuspendedTask] = []
private let nsLock = NSLock() // protects both of the non-isolated unsafe properties above


/// Initializes a new semaphore with a given concurrency limit.
Expand All @@ -90,17 +90,17 @@ public final class AsyncSemaphore: @unchecked Sendable {
///
/// Use this method when access to a resource should be awaited without the possibility of cancellation.
public func wait() async {
lock()
unsafeLock() // this is okay, as the continuation body actually runs sync, so we do no have async code within critical region

value -= 1
if value >= 0 {
unlock()
unsafeUnlock()
return
}

await withUnsafeContinuation { continuation in
suspendedTasks.append(SuspendedTask(id: UUID(), suspension: .regular(continuation)))
unlock()
nsLock.unlock()
}
}

Expand All @@ -112,19 +112,19 @@ public final class AsyncSemaphore: @unchecked Sendable {
public func waitCheckingCancellation() async throws {
try Task.checkCancellation() // check if we are already cancelled

lock()
unsafeLock() // this is okay, as the continuation body actually runs sync, so we do no have async code within critical region

do {
// check if we got cancelled while acquiring the lock
try Task.checkCancellation()
} catch {
unlock()
unsafeUnlock()
throw error
}

value -= 1 // decrease the value
if value >= 0 {
unlock()
unsafeUnlock()
return
}

Expand All @@ -134,28 +134,27 @@ public final class AsyncSemaphore: @unchecked Sendable {
try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation<Void, Error>) in
if Task.isCancelled {
value += 1 // restore the value
unlock()
unsafeUnlock()

continuation.resume(throwing: CancellationError())
} else {
suspendedTasks.append(SuspendedTask(id: id, suspension: .cancelable(continuation)))
unlock()
unsafeUnlock()
}
}
} onCancel: {
self.lock()

value += 1

guard let index = suspendedTasks.firstIndex(where: { $0.id == id }) else {
preconditionFailure("Inconsistent internal state reached")
let task = nsLock.withLock {
value += 1

guard let index = suspendedTasks.firstIndex(where: { $0.id == id }) else {
preconditionFailure("Inconsistent internal state reached")
}

let task = suspendedTasks[index]
suspendedTasks.remove(at: index)
return task
}

let task = suspendedTasks[index]
suspendedTasks.remove(at: index)

unlock()

switch task.suspension {
case .regular:
preconditionFailure("Tried to cancel a task that was not cancellable!")
Expand All @@ -173,18 +172,20 @@ public final class AsyncSemaphore: @unchecked Sendable {
/// - Returns: `true` if a task was resumed, `false` otherwise.
@discardableResult
public func signal() -> Bool {
lock()
let first: SuspendedTask? = nsLock.withLock {
value += 1

guard let first = suspendedTasks.first else {
return nil
}

value += 1
suspendedTasks.removeFirst()
return first
}

guard let first = suspendedTasks.first else {
unlock()
guard let first else {
return false
}

suspendedTasks.removeFirst()
unlock()

first.suspension.resume()
return true
}
Expand All @@ -193,14 +194,13 @@ public final class AsyncSemaphore: @unchecked Sendable {
///
/// This method resumes all `Task`s that are currently waiting for access.
public func signalAll() {
lock()

value += suspendedTasks.count
let tasks = nsLock.withLock {
value += suspendedTasks.count

let tasks = suspendedTasks
self.suspendedTasks.removeAll()

unlock()
let tasks = suspendedTasks
self.suspendedTasks.removeAll()
return tasks
}

for task in tasks {
task.suspension.resume()
Expand All @@ -213,14 +213,13 @@ public final class AsyncSemaphore: @unchecked Sendable {
///
/// - Warning: Will trigger a runtime error if it attempts to cancel `Task`s that are not cancellable.
public func cancelAll() {
lock()

value += suspendedTasks.count
let tasks = nsLock.withLock {
value += suspendedTasks.count

let tasks = suspendedTasks
self.suspendedTasks.removeAll()

unlock()
let tasks = suspendedTasks
self.suspendedTasks.removeAll()
return tasks
}

for task in tasks {
switch task.suspension {
Expand All @@ -232,11 +231,11 @@ public final class AsyncSemaphore: @unchecked Sendable {
}
}

private func lock() {
private func unsafeLock() { // silences a warning, just make sure that you don't have an await in between lock/unlock!
nsLock.lock()
}

private func unlock() {
private func unsafeUnlock() {
nsLock.unlock()
}
}
21 changes: 16 additions & 5 deletions Tests/SpeziFoundationTests/SharedRepositoryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ private protocol AnyTestInstance {

func testKeyLikeKnowledgeSource()

@MainActor
func testComputedKnowledgeSourceComputedOnlyPolicy()

@MainActor
func testComputedKnowledgeSourceComputedOnlyPolicyReadOnly()

@MainActor
func testComputedKnowledgeSourceStorePolicy()
}

Expand Down Expand Up @@ -66,8 +69,8 @@ final class SharedRepositoryTests: XCTestCase {
}

struct DefaultedTestStruct: DefaultProvidingKnowledgeSource, TestTypes {
var value: Int
static let defaultValue = DefaultedTestStruct(value: 0)
var value: Int
}

struct ComputedTestStruct<Policy: ComputedKnowledgeSourceStoragePolicy>: ComputedKnowledgeSource {
Expand All @@ -76,7 +79,9 @@ final class SharedRepositoryTests: XCTestCase {
typealias StoragePolicy = Policy

static func compute<Repository: SharedRepository<Anchor>>(from repository: Repository) -> Int {
computedValue
MainActor.assumeIsolated {
computedValue
}
}
}

Expand All @@ -86,7 +91,9 @@ final class SharedRepositoryTests: XCTestCase {
typealias StoragePolicy = Policy

static func compute<Repository: SharedRepository<Anchor>>(from repository: Repository) -> Int? {
optionalComputedValue
MainActor.assumeIsolated {
optionalComputedValue
}
}
}

Expand Down Expand Up @@ -254,11 +261,12 @@ final class SharedRepositoryTests: XCTestCase {
}
}

static var computedValue: Int = 3
static var optionalComputedValue: Int?
@MainActor static var computedValue: Int = 3
@MainActor static var optionalComputedValue: Int?

private var repos: [AnyTestInstance] = []

@MainActor
override func setUp() {
repos = [TestInstance(HeapRepository<TestAnchor>()), TestInstance(ValueRepository<TestAnchor>())]
Self.computedValue = 3
Expand Down Expand Up @@ -314,14 +322,17 @@ final class SharedRepositoryTests: XCTestCase {
repos.forEach { $0.testKeyLikeKnowledgeSource() }
}

@MainActor
func testComputedKnowledgeSourceComputedOnlyPolicy() {
repos.forEach { $0.testComputedKnowledgeSourceComputedOnlyPolicy() }
}

@MainActor
func testComputedKnowledgeSourceComputedOnlyPolicyReadOnly() {
repos.forEach { $0.testComputedKnowledgeSourceComputedOnlyPolicyReadOnly() }
}

@MainActor
func testComputedKnowledgeSourceStorePolicy() {
repos.forEach { $0.testComputedKnowledgeSourceStorePolicy() }
}
Expand Down
2 changes: 2 additions & 0 deletions Tests/SpeziFoundationTests/TimeoutTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import XCTest
final class TimeoutTests: XCTestCase {
@MainActor private var continuation: CheckedContinuation<Void, any Error>?

@MainActor
func operation(for duration: Duration) {
Task { @MainActor in
try? await Task.sleep(for: duration)
Expand All @@ -40,6 +41,7 @@ final class TimeoutTests: XCTestCase {
}
}

@MainActor
func testTimeout() async throws {
let negativeExpectation = XCTestExpectation()
negativeExpectation.isInverted = true
Expand Down

0 comments on commit 4781d96

Please sign in to comment.