Skip to content

Commit

Permalink
retry behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronsky committed Aug 31, 2024
1 parent 67ce11f commit a613a5f
Show file tree
Hide file tree
Showing 13 changed files with 266 additions and 32 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ do {

You can learn more about how to handle errors from the App Store Connect API at Apple's documentation page via [Interpreting and Handling Errors](https://developer.apple.com/documentation/appstoreconnectapi/interpreting_and_handling_errors). You can learn more about rate limiting at Apple's documentation page via [Identifying Rate Limits](https://developer.apple.com/documentation/appstoreconnectapi/identifying_rate_limits). Corresponding documentation for the Enterprise Program API can be found [here](https://developer.apple.com/documentation/enterpriseprogramapi/interpreting-and-handling-errors) and [here](https://developer.apple.com/documentation/enterpriseprogramapi/identifying-rate-limits), respectively.


Finally, `AppStoreConnectClient` can automatically retry API errors with a status code of 500. Pass a `RetryStrategy` value to the `retry:` parameter on any `AppStoreConnectClient` method. Supported values are `.never` (default), `.fixedInterval`, `.exponentialBackoff`, or a `.custom` handler.

### Paging Large Data Sets

All requests for resource collections (apps, builds, beta groups, etc.) support pagination. Responses for paginated resources will contain a `links` property of type `PagedDocumentLinks`, with "reference" URLs for `first`, `next`, and `self`. You can also find more information about the per-page limit and total count of resources in the response's `meta` field of type `PagingInformation`. You typically shouldn't require any of this information for typical pagination.
Expand All @@ -116,7 +119,7 @@ for try await appsPage in client.pages(Resources.v1.apps.get()) {
}
```

You can also page forward manually using the `send(_:pageAfter:)` method on `AppStoreConnectClient`.
You can also page forward manually using the `send(_:pageAfter:retry:)` method on `AppStoreConnectClient`.

### Uploading Assets

Expand Down
2 changes: 1 addition & 1 deletion Sources/AppStoreAPI/AppStoreAPI.docc/AppStoreAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The entire [publicly documented API surface](https://developer.apple.com/documen
### Uploading Files

- <doc:UploadingFiles>
- ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:)``
- ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:retry:)``
- ``UploadOperation``
- ``HTTPHeader``
- ``AppMediaAssetState``
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ let reserveScreenshot = try await client.send(
)
```

Using the screenshot response from the last step, unwrap the ``AppScreenshot/Attributes-swift.struct/uploadOperations`` array. While iterating on the array, provide the screenshot data and each operation in sequential order to ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:)``.
Using the screenshot response from the last step, unwrap the ``AppScreenshot/Attributes-swift.struct/uploadOperations`` array. While iterating on the array, provide the screenshot data and each operation in sequential order to ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:retry:)``.

```swift
guard let uploadOperations = reserveScreenshot.data.attributes?.uploadOperations else { return }
Expand Down Expand Up @@ -76,6 +76,6 @@ let committedScreenshot = try await client.send(

Committing the asset will cause App Store Connect to asynchronously process the asset. It may take several minutes to complete processing. You can perform ``Resources/V1-swift.struct/AppScreenshots-swift.struct/WithID/get(fieldsAppScreenshots:include:)`` to check its processing status and catch errors, which will be stored in ``AppScreenshot/Attributes-swift.struct/assetDeliveryState``. If your asset's state is set to ``AppMediaAssetState/State-swift.enum/failed``, you will need to restart this workflow to create a new screenshot with the errors addressed.

- ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:)``
- ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:retry:)``
- ``UploadOperation``
- ``AppMediaAssetState``
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

### Uploading Files

- ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:)``
- ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:retry:)``
11 changes: 9 additions & 2 deletions Sources/AppStoreAPI/UploadOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ extension AppStoreConnectClient {
/// - Parameters:
/// - operation: Information about the expected size of the chunk and its upload destination.
/// - data: The data representation of the uploaded resource.
/// - retry: Retry strategy.
/// - Throws: An error describing the manner in which the upload failed to complete.
public func upload(operation: UploadOperation, from data: Data) async throws {
public func upload(
operation: UploadOperation,
from data: Data,
retry strategy: some RetryStrategy = .never
) async throws {
guard let offset = operation.offset, let length = operation.length else {
throw UploadOperation.Error.chunkBoundsMismatch(offset: operation.offset, length: operation.length)
}
Expand All @@ -25,7 +30,9 @@ extension AppStoreConnectClient {
authenticator: &authenticator
)

_ = try await transport.upload(request: urlRequest, data: dataChunk, decoder: decoder)
_ = try await retry(with: strategy) {
try await transport.upload(request: urlRequest, data: dataChunk, decoder: decoder)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ This target is the client, networking layer, authentication layer, and queueing

### Error Handling

- ``RetryStrategy``
- ``Rate``
- ``ErrorResponse``

### Paging Large Data Sets

- ``AppStoreConnectClient/pages(_:)``
- ``AppStoreConnectClient/pages(_:retry:)``
- ``PagedResponses``
- ``PagedDocumentLinks``

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@

### Sending Requests

- ``send(_:)-4zqp1``
- ``send(_:)-5w42d``
- ``send(_:pageAfter:)``
- ``send(_:retry:)-4vcz8``
- ``send(_:retry:)-5bxw3``
- ``send(_:pageAfter:retry:)``

#### Pagination

- ``pages(_:)``
- ``send(_:pageAfter:)``
- ``pages(_:retry:)``
- ``send(_:pageAfter:retry:)``

### Downloading Files

- ``download(_:)``
- ``download(_:retry:)``

### Error Handling

- ``RetryStrategy``
- ``Rate``
- ``ErrorResponse``
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ Implementors of the ``Transport`` protocol provide the data transport layer to t

``AppStoreConnect`` provides a default, reasonably cross-platform implementation of ``Transport`` on ``Foundation/URLSession``. If you require support for other transport layers, such as [swift-nio](https://github.com/apple/swift-nio), it will be required to implement ``Transport`` yourself on your own types. Conforming to ``Transport`` implies a handful of responsibilities so that client behavior is consistent:

- The ``Response`` returned by each method should have ``Response/check()`` called before returning.
- The ``Response`` returned by each method should have ``Response/check()`` called before returning, so that ``RetryStrategy`` works reliably.
- If the response from the API is malformed prior to decoding, such as in a situation where the response is not HTTP, consider throwing ``TransportError/unrecognizedResponse``.
- The client does not do any additional caching, nor does it intentionally check against the `Last-Modified` HTTP header coming from the APIs. If you wish to introduce a caching mechanism for responses, it should be done in the ``Transport`` layer.
67 changes: 53 additions & 14 deletions Sources/AppStoreConnect/AppStoreConnectClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,55 +48,85 @@ public actor AppStoreConnectClient {
// MARK: - Requests

/// Performs the given request asynchronously.
/// - Parameter request: A request.
///
/// - Parameters:
/// - request: A request.
/// - retry: Retry strategy.
/// - Throws: An error describing the manner in which the request failed to complete.
public func send(_ request: Request<Void>) async throws {
public func send(
_ request: Request<Void>,
retry strategy: some RetryStrategy = .never
) async throws {
let urlRequest = try URLRequest(request: request, encoder: encoder, authenticator: &authenticator)
_ = try await transport.send(request: urlRequest, decoder: decoder)

_ = try await retry(with: strategy) {
try await transport.send(request: urlRequest, decoder: decoder)
}
}

/// Performs the given request asynchronously.
/// - Parameter request: A request.
///
/// - Parameters:
/// - request: A request.
/// - retry: Retry strategy.
/// - Returns: The response from the App Store Connect API.
/// - Throws: An error describing the manner in which the request failed to complete.
public func send<Response>(_ request: Request<Response>) async throws -> Response where Response: Decodable {
public func send<Response>(
_ request: Request<Response>,
retry strategy: some RetryStrategy = .never
) async throws -> Response where Response: Decodable {
let urlRequest = try URLRequest(request: request, encoder: encoder, authenticator: &authenticator)
let response = try await transport.send(request: urlRequest, decoder: decoder)

let response = try await retry(with: strategy) {
try await transport.send(request: urlRequest, decoder: decoder)
}

return try response.decode(using: decoder)
}

// MARK: - Pagination

/// Convenience method for accessing a series of paged resources in a sequence asynchronously.
/// - Parameter request: The initial request of the sequence.
///
/// - Parameters:
/// - request: The initial request of the sequence.
/// - retry: Retry strategy.
/// - Returns: A ``PagedResponses`` sequence which will provide with each page's response asynchronously.
public nonisolated func pages<Response>(_ request: Request<Response>) -> PagedResponses<Response> {
PagedResponses(request: request, client: self)
public nonisolated func pages<Response>(
_ request: Request<Response>,
retry strategy: some RetryStrategy = .never
) -> PagedResponses<Response> {
PagedResponses(request: request, client: self, retry: strategy)
}

/// Performs the request that was used to fetch the current object to fetch the next page asynchronously.
///
/// - Parameters:
/// - request: A request.
/// - currentPage: The API object representing the current page.
/// - retry: Retry strategy.
/// - Returns: The response from the App Store Connect API.
/// - Throws: An error describing the manner in which the request failed to complete.
public func send<Response>(
_ request: Request<Response>,
pageAfter currentPage: Response
pageAfter currentPage: Response,
retry strategy: some RetryStrategy = .never
) async throws -> Response? where Response: Decodable {
guard let nextPage = pagedDocumentLinks(currentPage)?.next else {
return nil
}

let urlRequest = try URLRequest(url: nextPage, encoder: encoder, authenticator: &authenticator)
let response = try await transport.send(request: urlRequest, decoder: decoder)

let response = try await retry(with: strategy) {
try await transport.send(request: urlRequest, decoder: decoder)
}

return try response.decode(using: decoder)
}

/// Performs the given request asynchronously.
///
/// - Parameter object: Some object that may contain a property of the type ``PagedDocumentLinks``.
/// - Returns: The ``PagedDocumentLinks`` instance, if one exists.
private nonisolated func pagedDocumentLinks<Entity>(_ object: Entity) -> PagedDocumentLinks? {
Expand All @@ -108,12 +138,21 @@ public actor AppStoreConnectClient {
// MARK: - Downloads

/// Downloads a resource asynchronously to a temporary location.
/// - Parameter request: A request.
///
/// - Parameters:
/// - request: A request.
/// - retry: Retry strategy.
/// - Returns: URL to the location of the resource on-disk.
/// - Throws: An error describing the manner in which the request failed to complete.
public func download(_ request: Request<Data>) async throws -> URL {
public func download(
_ request: Request<Data>,
retry strategy: some RetryStrategy = .never
) async throws -> URL {
let urlRequest = try URLRequest(request: request, encoder: encoder, authenticator: &authenticator)
let response = try await transport.download(request: urlRequest)

let response = try await retry(with: strategy) {
try await transport.download(request: urlRequest)
}

return try response.decode()
}
Expand Down
10 changes: 7 additions & 3 deletions Sources/AppStoreConnect/Networking/Pagination.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@ public struct PagedResponses<Response: Decodable & Sendable>: AsyncSequence, Asy
let request: Request<Response>
/// A reference to the API client.
let client: AppStoreConnectClient
/// The retry strategy.
let retryStrategy: any RetryStrategy

/// Creates the sequence.
/// - Parameters:
/// - request: The initial request.
/// - client: The API client.
init(
request: Request<Response>,
client: AppStoreConnectClient
client: AppStoreConnectClient,
retry strategy: some RetryStrategy
) {
self.request = request
self.client = client
self.retryStrategy = strategy
}

private var currentElement: Element?
Expand All @@ -39,9 +43,9 @@ public struct PagedResponses<Response: Decodable & Sendable>: AsyncSequence, Asy
}

if let current = currentElement {
currentElement = try await client.send(request, pageAfter: current)
currentElement = try await client.send(request, pageAfter: current, retry: retryStrategy)
} else {
currentElement = try await client.send(request)
currentElement = try await client.send(request, retry: retryStrategy)
}

return currentElement
Expand Down
Loading

0 comments on commit a613a5f

Please sign in to comment.