generated from StanfordBDHG/SwiftPackageTemplate
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add infrastructure for timeouts (#9)
# Add infrastructure for timeouts ## ♻️ Current situation & Problem Async operations sometimes require to make sure that there is a maximum time an operations runs. To have a standardized way of dealing with timeouts, this PR introduces a new, generalized `TimeoutError` (similarly simple like Swift's `CancellationError`) to communicate that a timeout occurred. Additional, the PR adds a `withTimeout` method that makes it easier to race timeouts against an async operation. ## ⚙️ Release Notes * Add `TimeoutError` * Add `withTimeout(of:perform:)` method ## 📚 Documentation Code examples have been added to illustrate how to use those new types. ## ✅ Testing Unit testing was added to verify functionality. ## 📝 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
Showing
5 changed files
with
210 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// | ||
// This source file is part of the Stanford Spezi open-source project | ||
// | ||
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
import Foundation | ||
|
||
|
||
/// Timeout occurred inside an async operation. | ||
public struct TimeoutError { | ||
/// Create a new timeout error. | ||
public init() {} | ||
} | ||
|
||
|
||
extension TimeoutError: Error {} | ||
|
||
|
||
/// Race a timeout. | ||
/// | ||
/// This method can be used to race an operation against a timeout. | ||
/// | ||
/// ### Timeout in Async Context | ||
/// | ||
/// Below is a code example showing how to best use the `withTimeout(of:perform:)` method in an async method. | ||
/// The example uses [Structured Concurrency](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency) | ||
/// creating a child task running the timeout task. This makes sure that the timeout is automatically cancelled when the method goes out of scope. | ||
/// | ||
/// - Note: The example isolates the `continuation` property to the MainActor to ensure accesses are synchronized. | ||
/// Further, the method throws an error if the operation is already running. We use the `OperationAlreadyInUseError` | ||
/// error as an example. | ||
/// | ||
/// ```swift | ||
/// @MainActor | ||
/// var operation: CheckedContinuation<Void, Error>? | ||
/// | ||
/// @MainActor | ||
/// func foo() async throws { | ||
/// guard continuation == nil else { | ||
/// throw OperationAlreadyInUseError() // exemplary way of handling concurrent accesses | ||
/// } | ||
/// | ||
/// async let _ = withTimeout(of: .seconds(30)) { @MainActor in | ||
/// // operation timed out, resume continuation by throwing a `TimeoutError`. | ||
/// if let continuation = operation { | ||
/// operation = nil | ||
/// continuation.resume(throwing: TimeoutError()) | ||
/// } | ||
/// } | ||
/// | ||
/// runOperation() | ||
/// try await withCheckedThrowingContinuation { continuation in | ||
/// self.continuation = continuation | ||
/// } | ||
/// } | ||
/// | ||
/// @MainActor | ||
/// func handleOperationCompleted() { | ||
/// if let continuation = operation { | ||
/// operation = nil | ||
/// continuation.resume() | ||
/// } | ||
/// } | ||
/// ``` | ||
/// | ||
/// ### Timeout in Sync Context | ||
/// | ||
/// Using `withTimeout(of:perform:)` in a synchronous method is similar. However, you will need to take care of cancellation yourself. | ||
/// | ||
/// ```swift | ||
/// func foo() throws { | ||
/// let timeoutTask = Task { | ||
/// await withTimeout(of: .seconds(30)) { | ||
/// // cancel operation ... | ||
/// } | ||
/// } | ||
/// | ||
/// defer { | ||
/// timeoutTask.cancel() | ||
/// } | ||
/// | ||
/// try operation() | ||
/// } | ||
/// ``` | ||
/// | ||
/// - 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 { | ||
try? await Task.sleep(for: timeout) | ||
guard !Task.isCancelled else { | ||
return | ||
} | ||
|
||
await action() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# System Programming Interfaces | ||
|
||
<!-- | ||
# | ||
# This source file is part of the Stanford Spezi open-source project | ||
# | ||
# SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
# | ||
# SPDX-License-Identifier: MIT | ||
# | ||
--> | ||
|
||
An overview of System Programming Interfaces (SPIs) provided by Spezi Foundation. | ||
|
||
## Overview | ||
|
||
A [System Programming Interface](https://blog.eidinger.info/system-programming-interfaces-spi-in-swift-explained) is a subset of API | ||
that is targeted only for certain users (e.g., framework developers) and might not be necessary or useful for app development. | ||
Therefore, these interfaces are not visible by default and need to be explicitly imported. | ||
This article provides an overview of supported SPI provided by SpeziFoundation | ||
|
||
### TestingSupport | ||
|
||
The `TestingSupport` SPI provides additional interfaces that are useful for unit and UI testing. | ||
Annotate your import statement as follows. | ||
|
||
```swift | ||
@_spi(TestingSupport) import SpeziFoundation | ||
``` | ||
|
||
- Note: As of Swift 5.8, you can solely import the SPI target without any other interfaces of the SPM target | ||
by setting the `-experimental-spi-only-imports` Swift compiler flag and using `@_spiOnly`. | ||
|
||
```swift | ||
@_spiOnly import SpeziFoundation | ||
``` | ||
|
||
#### RuntimeConfig | ||
|
||
The `RuntimeConfig` stores configurations of the current runtime environment for testing support. | ||
|
||
- `RuntimeConfig/testMode`: Holds `true` if the `--testMode` command line flag was supplied to indicate to enable additional testing functionalities. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
// | ||
// This source file is part of the Stanford Spezi open-source project | ||
// | ||
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
|
||
import SpeziFoundation | ||
import XCTest | ||
|
||
|
||
final class TimeoutTests: XCTestCase { | ||
@MainActor private var continuation: CheckedContinuation<Void, any Error>? | ||
|
||
func operation(for duration: Duration) { | ||
Task { @MainActor in | ||
try? await Task.sleep(for: duration) | ||
if let continuation = self.continuation { | ||
continuation.resume() | ||
self.continuation = nil | ||
} | ||
} | ||
} | ||
|
||
@MainActor | ||
func operationMethod(timeout: Duration, operation: Duration, timeoutExpectation: XCTestExpectation) async throws { | ||
async let _ = withTimeout(of: timeout) { @MainActor in | ||
timeoutExpectation.fulfill() | ||
if let continuation { | ||
continuation.resume(throwing: TimeoutError()) | ||
self.continuation = nil | ||
} | ||
} | ||
|
||
try await withCheckedThrowingContinuation { continuation in | ||
self.continuation = continuation | ||
self.operation(for: operation) | ||
} | ||
} | ||
|
||
func testTimeout() async throws { | ||
let negativeExpectation = XCTestExpectation() | ||
negativeExpectation.isInverted = true | ||
try await operationMethod(timeout: .seconds(1), operation: .milliseconds(500), timeoutExpectation: negativeExpectation) | ||
|
||
|
||
await fulfillment(of: [negativeExpectation], timeout: 2) | ||
|
||
let expectation = XCTestExpectation() | ||
do { | ||
try await operationMethod(timeout: .milliseconds(500), operation: .seconds(5), timeoutExpectation: expectation) | ||
XCTFail("Operation did unexpectedly complete!") | ||
} catch { | ||
XCTAssert(error is TimeoutError) | ||
} | ||
await fulfillment(of: [expectation]) | ||
} | ||
} |