Skip to content

Commit

Permalink
Swift Concurrency Support (Alamofire#3463)
Browse files Browse the repository at this point in the history
* Add Concurrency support.

* Remove misleading Protected convenience APIs.

* Safer NSLog usage.

* Project updates.

* Disable thread sanitizer during development.

* Update tests for new APIs.

* Add concurrency tester.

* Fix concurrency runner.

* 15.2 simulator for concurrency tests.

* Disable thread sanitizer on iOS as well.

* Update inline docs, add stream cancellation.

* Add documentation.

* Document async value handlers.

* Remove TODO.

* Add doc comment.

* Target the concurrent queue for the streams.

* Rename finishObservers to finishHandlers.
  • Loading branch information
jshier authored Dec 13, 2021
1 parent 65b7769 commit 0c3780e
Show file tree
Hide file tree
Showing 14 changed files with 1,482 additions and 93 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,21 @@ jobs:
run: arch -arch arm64e brew install alamofire/alamofire/firewalk || arch -arch arm64e brew upgrade alamofire/alamofire/firewalk && arch -arch x86_64 firewalk &
- name: iOS - ${{ matrix.destination }}
run: set -o pipefail && arch -arch arm64e env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire iOS" -destination "${{ matrix.destination }}" clean test | xcpretty
iOS_Concurrency:
name: Test Swift Concurrency
runs-on: macOS-11
env:
DEVELOPER_DIR: /Applications/Xcode_13.2.app/Contents/Developer
timeout-minutes: 10
strategy:
matrix:
destination: ["OS=15.2,name=iPhone 13 Pro"]
steps:
- uses: actions/checkout@v2
- name: Install Firewalk
run: brew install alamofire/alamofire/firewalk || brew upgrade alamofire/alamofire/firewalk && firewalk &
- name: iOS - ${{ matrix.destination }}
run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire iOS" -destination "${{ matrix.destination }}" -enableThreadSanitizer NO clean test | xcpretty
tvOS:
name: Test tvOS
runs-on: firebreak
Expand Down
22 changes: 20 additions & 2 deletions Alamofire.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 53;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -187,6 +187,13 @@
319917BA209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */; };
319917BB209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */; };
319917BC209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */; };
31B3DE3B25C11CEA00760641 /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE3A25C11CEA00760641 /* Concurrency.swift */; };
31B3DE3C25C11CEA00760641 /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE3A25C11CEA00760641 /* Concurrency.swift */; };
31B3DE3D25C11CEA00760641 /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE3A25C11CEA00760641 /* Concurrency.swift */; };
31B3DE3E25C11CEA00760641 /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE3A25C11CEA00760641 /* Concurrency.swift */; };
31B3DE4F25C120D800760641 /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE4E25C120D800760641 /* ConcurrencyTests.swift */; };
31B3DE5025C120D800760641 /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE4E25C120D800760641 /* ConcurrencyTests.swift */; };
31B3DE5125C120D800760641 /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE4E25C120D800760641 /* ConcurrencyTests.swift */; };
31B51E8C2434FECB005356DB /* RequestModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B51E8B2434FECB005356DB /* RequestModifierTests.swift */; };
31B51E8D2434FECB005356DB /* RequestModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B51E8B2434FECB005356DB /* RequestModifierTests.swift */; };
31B51E8E2434FECB005356DB /* RequestModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B51E8B2434FECB005356DB /* RequestModifierTests.swift */; };
Expand Down Expand Up @@ -479,6 +486,8 @@
319ECEA425EC96E8001C38CA /* config.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = config.yml; sourceTree = "<group>"; };
319ECEA525EC9710001C38CA /* FEATURE_REQUEST.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = FEATURE_REQUEST.md; sourceTree = "<group>"; };
31B2CA9521AA25CD005B371A /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
31B3DE3A25C11CEA00760641 /* Concurrency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Concurrency.swift; sourceTree = "<group>"; };
31B3DE4E25C120D800760641 /* ConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrencyTests.swift; sourceTree = "<group>"; };
31B51E8B2434FECB005356DB /* RequestModifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestModifierTests.swift; sourceTree = "<group>"; };
31BADE4D2439A8D1007D2AB9 /* CombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineTests.swift; sourceTree = "<group>"; };
31D83FCD20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLConvertible+URLRequestConvertible.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -697,6 +706,7 @@
4CFD6B132201338E00FFB5E3 /* CachedResponseHandlerTests.swift */,
4C341BB91B1A865A00C1B34D /* CacheTests.swift */,
31BADE4D2439A8D1007D2AB9 /* CombineTests.swift */,
31B3DE4E25C120D800760641 /* ConcurrencyTests.swift */,
3113D46A21878227001CCD21 /* HTTPHeadersTests.swift */,
4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */,
4C3D00571C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift */,
Expand Down Expand Up @@ -882,6 +892,7 @@
4C67D1352454B12A00CBA725 /* AuthenticationInterceptor.swift */,
4C4466EA21F8F5D800AC9703 /* CachedResponseHandler.swift */,
318DD40E2439780500963291 /* Combine.swift */,
31B3DE3A25C11CEA00760641 /* Concurrency.swift */,
3111CE8720A77843008315E2 /* EventMonitor.swift */,
4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */,
311B198F20B0D3B40036823B /* MultipartUpload.swift */,
Expand Down Expand Up @@ -1262,7 +1273,7 @@
};
};
buildConfigurationList = F8111E2D19A95C8B0040E7D1 /* Build configuration list for PBXProject "Alamofire" */;
compatibilityVersion = "Xcode 11.4";
compatibilityVersion = "Xcode 12.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Expand Down Expand Up @@ -1520,6 +1531,7 @@
3191B5771F5F53A6003960A8 /* Protected.swift in Sources */,
3199179A209CDA7F00103A19 /* Response.swift in Sources */,
31D83FD020D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */,
31B3DE3D25C11CEA00760641 /* Concurrency.swift in Sources */,
319917A7209CDAC400103A19 /* RequestTaskMap.swift in Sources */,
4CF627131BA7CBF60011A099 /* Validation.swift in Sources */,
31F5085F20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */,
Expand Down Expand Up @@ -1590,6 +1602,7 @@
31C2B0EC20B271060089BA7C /* CacheTests.swift in Sources */,
3111CE9120A7EC27008315E2 /* NetworkReachabilityManagerTests.swift in Sources */,
31BADE502439A8D1007D2AB9 /* CombineTests.swift in Sources */,
31B3DE5125C120D800760641 /* ConcurrencyTests.swift in Sources */,
31762DCC247738FA0025C704 /* LeaksTests.swift in Sources */,
31425AC3241F098000EE3CCC /* InternalRequestTests.swift in Sources */,
4CF627171BA7CC240011A099 /* ParameterEncodingTests.swift in Sources */,
Expand All @@ -1604,6 +1617,7 @@
3191B5761F5F53A6003960A8 /* Protected.swift in Sources */,
4CDE2C471AF89FF300BABAE5 /* ResponseSerialization.swift in Sources */,
31991799209CDA7F00103A19 /* Response.swift in Sources */,
31B3DE3C25C11CEA00760641 /* Concurrency.swift in Sources */,
31D83FCF20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */,
319917A6209CDAC400103A19 /* RequestTaskMap.swift in Sources */,
4C1DC8551B68908E00476DE3 /* AFError.swift in Sources */,
Expand Down Expand Up @@ -1646,6 +1660,7 @@
E4202FD01B667AA100C997FB /* ParameterEncoding.swift in Sources */,
3191B5781F5F53A6003960A8 /* Protected.swift in Sources */,
3199179B209CDA7F00103A19 /* Response.swift in Sources */,
31B3DE3E25C11CEA00760641 /* Concurrency.swift in Sources */,
31D83FD120D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */,
319917A8209CDAC400103A19 /* RequestTaskMap.swift in Sources */,
4CEC605A1B745C9100E684F4 /* AFError.swift in Sources */,
Expand Down Expand Up @@ -1688,6 +1703,7 @@
3191B5751F5F53A6003960A8 /* Protected.swift in Sources */,
4CDE2C461AF89FF300BABAE5 /* ResponseSerialization.swift in Sources */,
31991798209CDA7F00103A19 /* Response.swift in Sources */,
31B3DE3B25C11CEA00760641 /* Concurrency.swift in Sources */,
31D83FCE20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */,
319917A5209CDAC400103A19 /* RequestTaskMap.swift in Sources */,
4C1DC8541B68908E00476DE3 /* AFError.swift in Sources */,
Expand Down Expand Up @@ -1758,6 +1774,7 @@
31C2B0EA20B271040089BA7C /* CacheTests.swift in Sources */,
3111CE8F20A7EC26008315E2 /* NetworkReachabilityManagerTests.swift in Sources */,
31BADE4E2439A8D1007D2AB9 /* CombineTests.swift in Sources */,
31B3DE4F25C120D800760641 /* ConcurrencyTests.swift in Sources */,
31762DCA247738FA0025C704 /* LeaksTests.swift in Sources */,
31425AC1241F098000EE3CCC /* InternalRequestTests.swift in Sources */,
F8111E6119A9674D0040E7D1 /* ParameterEncodingTests.swift in Sources */,
Expand Down Expand Up @@ -1800,6 +1817,7 @@
31C2B0EB20B271050089BA7C /* CacheTests.swift in Sources */,
3111CE9020A7EC27008315E2 /* NetworkReachabilityManagerTests.swift in Sources */,
31BADE4F2439A8D1007D2AB9 /* CombineTests.swift in Sources */,
31B3DE5025C120D800760641 /* ConcurrencyTests.swift in Sources */,
31762DCB247738FA0025C704 /* LeaksTests.swift in Sources */,
31425AC2241F098000EE3CCC /* InternalRequestTests.swift in Sources */,
4C256A541B096C770065714F /* BaseTestCase.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "NO"
enableThreadSanitizer = "YES"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
<MacroExpansion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "NO"
enableASanStackUseAfterReturn = "YES"
enableThreadSanitizer = "YES"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
<MacroExpansion>
Expand Down
124 changes: 124 additions & 0 deletions Documentation/AdvancedUsage.md
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,130 @@ Like most `DownloadRequest`'s response handlers, `DownloadResponsePublisher` rea
#### `DataStreamPublisher`
`DataStreamPublisher` is a `Publisher` for `DataStreamRequest`s. Like `DataStreamRequest` itself, and unlike Alamofire's other `Publisher`s, `DataStreamPublisher` can return multiple values serialized from `Data` received from the network, as well as a final completion event. For more information on how `DataStreamRequest` works, please see our [detailed usage documentation](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#streaming-data-from-a-server).

## Using Alamofire with Swift Concurrency
Swift's concurrency features, released in Swift 5.5, provide fundamental asynchronous building blocks in the language, including `async`-`await` syntax, `Task`s, and actors. Alamofire provides extensions allowing the use of common Alamofire APIs with Swift's concurrency features.

> Alamofire's concurrency support requires Swift 5.5.2 or Xcode 13.2. These examples also include the use of static protocol values added in Alamofire 5.5 for Swift 5.5.

Alamofire's concurrency support works by vending various `*Task` types, like `DataTask`, `DownloadTask`, and `DataStreamTask`. These types work similarly to Alamofire's existing response handlers and convert the standard completion handlers into `async` properties which can be `await`ed. For example, `DataRequest` (and `UploadRequest`, which inherits from `DataRequest`) can provide a `DataTask` used to `await` any of the asynchronous values:

```swift
let value = try await AF.request(...).serializingDecodable(TestResponse.self).value
```

This code synchronously produces a `DataTask<TestResponse>` value which can be used to `await` any part of the resulting `DataResponse<TestResponse, AFError>`. Each `DataTask` can be used to `await` any of these properties as many times as needed. For example:

```swift
let dataTask = AF.request(...).serializingDecodable(TestResponse.self)
// Later...
let response = await task.response // Returns full DataResponse<TestResponse, AFError>
// Elsewhere...
let result = await task.result // Returns Result<TestResponse, AFError>
// And...
let value = try await task.value // Returns the TestResponse or throws the AFError
```

Like all Swift Concurrency APIs, these `await`able properties can be used to `await` multiple requests issued in parallel. For example:

```swift
async let first = AF.request(...).serializingDecodable(TestResponse.self).response
async let second = AF.request(...).serializingString().response
async let third = AF.request(...).serializingData().response

// Later...

// Produces (DataResponse<TestResponse, AFError>, DataResponse<String, AFError>, DataResponse<Data, AFError>)
// when all requests are complete.
let responses = await (first, second, third)
```

Alamofire's concurrency APIs can also be used with other builtin concurrency constructs like `Task` and `TaskGroup`.

### `DownloadRequest` Support

Like `DataRequest`, `DownloadRequest` vends its own `DownloadTask` value which can be used to `await` the completion of the request. Like the existing response handlers, the `DownloadTask` will read the downloaded `Data` from disk, so if the `Data` is very large it's best to simply get the `URL` and read the `Data` in a way that won't read it all into memory at once.

```swift
let url = try await AF.download(...).serializingURL().value
```

#### Automatic Cancellation

By default, `DataTask` and `DownloadTask` values do not cancel the underlying request when an enclosing concurrent context is cancelled. This means that request will complete even if the enclosing context is explicitly cancelled. For example:

```swift
let request = AF.request(...) // Creates the DataRequest.
let task = Task { // Produces a `Task<DataResponse<TestResponse, AFError>, Never> value.
await request.serializingDecodable(TestResponse.self, automaticallyCancelling: true).response
}

// Later...

task.cancel() // task is cancelled, but the DataRequest created inside it is not.
print(task.isCancelled) // true
print(request.isCancelled) // false
```

If automatic cancellation is desired, it can be configured when creating the `DataTask` or `DownloadTask`. For example:

```swift
let request = AF.request(...) // Creates the DataRequest.
let task = Task { // Produces a `Task<DataResponse<TestResponse, AFError>, Never> value.
await request.serializingDecodable(TestResponse.self, automaticallyCancelling: true).response
}

// Later...

task.cancel() // task is cancelled.
print(task.isCancelled) // true
print(request.isCancelled) // true
```

This automatic cancellation only takes affect when one of the asynchronous properties is `await`ed.

### `DataStreamRequest` Support

`DataStreamRequest`, unlike the other request types, does not read a single value and complete. Instead, it continuously streams `Data` from the server to be processed through a handler. With Swift Concurrency, this callback API has been replaced with `StreamOf` values vended by `DataStreamTask`. `StreamOf` conforms to `AsyncSequence`, allowing the use of `for await` syntax to observe values as they're received by the stream. Unlike `DataTask` and `DownloadTask`, `DataStreamTask` doesn't vend asynchronous properties itself. Instead, it vends the streams that can be observed.

```swift
let streamTask = AF.dataStreamRequest(...).streamTask()

// Later...

for await data in streamTask.streamingData() {
// Streams Stream<Data, Never> values. a.k.a StreamOf<DataStreamRequest.Stream<Data, Never>>
}
```

This loop only ends when the `DataStreamRequest` completes, either through the server closing the connection or the `DataStreamRequest` being cancelled. If the loop is ended early by `break`ing out of it, the `DataStreamRequest` is canceled and no further values can be received. If the use of multiple observers without automatically cancellation is desired, you can pass `false` for the `automaticallyCancelling` parameter.

```swift
let streamTask = AF.dataStreamRequest(...).streamTask()

// Later...

for await data in streamTask.streamingData(automaticallyCancelling: false) {
// Streams Stream<Data, Never> values. a.k.a StreamOf<DataStreamRequest.Stream<Data, Never>>
if condition { break } // Stream ends but underlying `DataStreamRequest` is not cancelled and keeps receiving data.
}
```

One observer setting `automaticallyCancelling` to `false` does not affect other from the same `DataStreamRequest`, so if any other observer exits the request will still be cancelled.

### Value Handlers

Alamofire provides various handlers for internal values which are produced asynchronously, such as `Progress` values, `URLRequest`s and `URLSessionTask`s, as well as cURL descriptions of the request each time a new request is issued. Alamofire's concurrency support now exposes these handlers as `StreamOf` values that can be used to asynchronously observe the received values. For instance, if you wanted to print each cURL description produced by a request:

```swift
let request = AF.request(...)

// Later...

for await description in request.cURLDescriptions() {
print(description)
}
```

## Network Reachability
The `NetworkReachabilityManager` listens for changes in the reachability of hosts and addresses for both Cellular and WiFi network interfaces.

Expand Down
17 changes: 16 additions & 1 deletion Example/iOS Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
31AB09122733010700986A70 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 31E4765F1C55DD5900968569 /* Alamofire.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 31293065263E17D600473CEA;
remoteInfo = "Alamofire watchOS Tests";
};
31E476691C55DD5900968569 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 31E4765F1C55DD5900968569 /* Alamofire.xcodeproj */;
Expand Down Expand Up @@ -124,6 +131,7 @@
31E476721C55DD5900968569 /* Alamofire.framework */,
31E476741C55DD5900968569 /* Alamofire tvOS Tests.xctest */,
31E476761C55DD5900968569 /* Alamofire.framework */,
31AB09132733010700986A70 /* Alamofire watchOS Tests.xctest */,
);
name = Products;
sourceTree = "<group>";
Expand Down Expand Up @@ -208,7 +216,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0720;
LastUpgradeCheck = 1250;
LastUpgradeCheck = 1320;
ORGANIZATIONNAME = Alamofire;
TargetAttributes = {
F8111E0419A951050040E7D1 = {
Expand Down Expand Up @@ -242,6 +250,13 @@
/* End PBXProject section */

/* Begin PBXReferenceProxy section */
31AB09132733010700986A70 /* Alamofire watchOS Tests.xctest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
path = "Alamofire watchOS Tests.xctest";
remoteRef = 31AB09122733010700986A70 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
31E4766A1C55DD5900968569 /* Alamofire.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
Expand Down
Loading

0 comments on commit 0c3780e

Please sign in to comment.