Skip to content
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

change APIProvider to be an actor instead of a class #242

Closed
wants to merge 1 commit into from

Conversation

nickasd
Copy link
Contributor

@nickasd nickasd commented Sep 4, 2023

Solves #238.

@nickasd nickasd requested a review from AvdLee as a code owner September 4, 2023 09:33
@nickasd
Copy link
Contributor Author

nickasd commented Sep 4, 2023

I just followed your instructions on how to run the tests and noticed that I still had to change the tests.

I don't know if I'm doing something wrong, but running bundle exec fastlane test first gave an error

bundler: command not found: fastlane
Install missing gem executables with `bundle install`

But running bundle install also wasn't sufficient. I had to add a line to Gemfile:

gem "fastlane"

Then I was able to run the tests. But they fail in the Terminal:

xctest (95287) encountered an error (Failed to create a bundle instance representing 'appstoreconnect-swift-sdk/build/derived_data/Build/Products/Debug-iphonesimulator/AppStoreConnect-Swift-SDK-Tests.xctest'. Check that the bundle exists on disk. If you believe this error represents a bug, please attach the result bundle at appstoreconnect-swift-sdk/build/reports/AppStoreConnect-Swift-SDK.xcresult)

In fact, when opening appstoreconnect-swift-sdk/.swiftpm/xcode/package.xcworkspace in Xcode and running the tests with Command-U, I also get an error:

Logic Testing Unavailable Logic Testing on iOS devices is not supported. You can run logic tests on the Simulator.

I had to change the target to My Mac and then the tests all passed. But I don't know what I have to do in order to be able to run the tests in the Terminal.

@SwiftLeeBot
Copy link
Collaborator

Warnings
⚠️ 'Prices' is deprecated: Deprecated
⚠️ 'AvailableTerritories' is deprecated: Deprecated
⚠️ 'InAppPurchases' is deprecated: Deprecated
⚠️ 'Prices' is deprecated: Deprecated
⚠️ 'AvailableTerritories' is deprecated: Deprecated
⚠️ 'InAppPurchases' is deprecated: Deprecated
⚠️ 'Builds' is deprecated: Deprecated
⚠️ 'Builds' is deprecated: Deprecated
⚠️ 'AppPrice' is deprecated: Deprecated
⚠️ 'AgeRatingDeclaration' is deprecated: Deprecated
⚠️ 'AgeRatingDeclaration' is deprecated: Deprecated
⚠️ 'AppStoreVersionExperiment' is deprecated: Deprecated
⚠️ 'AppStoreVersionExperiment' is deprecated: Deprecated
⚠️ 'AppStoreVersionSubmission' is deprecated: Deprecated
⚠️ 'AppStoreVersionExperiment' is deprecated: Deprecated
⚠️ 'AppStoreVersionSubmission' is deprecated: Deprecated
⚠️ 'AppStoreVersionExperiment' is deprecated: Deprecated
⚠️ 'Prices' is deprecated: Deprecated
⚠️ 'AvailableTerritories' is deprecated: Deprecated
⚠️ 'Prices' is deprecated: Deprecated
⚠️ 'AvailableTerritories' is deprecated: Deprecated
⚠️ 'AppPrice' is deprecated: Deprecated
⚠️ 'AppStoreVersionExperiment' is deprecated: Deprecated
⚠️ 'AppStoreVersionExperiment' is deprecated: Deprecated
⚠️ 'Builds' is deprecated: Deprecated
⚠️ 'AppStoreVersionSubmission' is deprecated: Deprecated
⚠️ 'AppStoreVersionExperiment' is deprecated: Deprecated
⚠️ 'AppStoreVersionSubmission' is deprecated: Deprecated
⚠️ 'AppStoreVersionExperiment' is deprecated: Deprecated
⚠️ 'Prices' is deprecated: Deprecated
⚠️ 'AvailableTerritories' is deprecated: Deprecated
⚠️ 'AppPrice' is deprecated: Deprecated
⚠️ 'AgeRatingDeclaration' is deprecated: Deprecated
⚠️ 'AppStoreVersionExperiment' is deprecated: Deprecated
⚠️ 'AppStoreVersionExperiment' is deprecated: Deprecated
⚠️ 'AppPrice' is deprecated: Deprecated
⚠️ Left side of nil coalescing operator '??' has non-optional type 'String', so the right side is never used
⚠️ 'Prices' is deprecated: Deprecated
⚠️ 'AvailableTerritories' is deprecated: Deprecated
⚠️ 'InAppPurchases' is deprecated: Deprecated
⚠️ 'AppStoreVersionExperiment' is deprecated: Deprecated
⚠️ 'AppStoreVersionExperiment' is deprecated: Deprecated
Messages
📖 AppStoreConnect-Swift-SDK-Tests: Executed 17 tests (0 failed, 0 retried, 0 skipped) in 0.307 seconds
📖

View more details on Bitrise

Code Coverage Report

Name Coverage

SwiftLint found issues

Severity File Reason
Warning APIProviderTests.swift:13 Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon)
Warning APIProviderTests.swift:43 Method 'setUp()' should call to super function (overridden_super_call)
Warning APIProviderTests.swift:9 Imports should be sorted (sorted_imports)
Warning APIProvider.swift:76 Use shorthand syntax for optional binding (shorthand_optional_binding)
Warning APIProvider.swift:138 Lines should not have trailing whitespace (trailing_whitespace)
Warning APIProvider.swift:244 Lines should not have trailing whitespace (trailing_whitespace)
Warning APIProvider.swift:274 Lines should not have trailing whitespace (trailing_whitespace)
Warning APIProvider.swift:295 Lines should not have trailing whitespace (trailing_whitespace)
Warning APIProvider.swift:312 Lines should not have trailing whitespace (trailing_whitespace)
Warning APIProvider.swift:322 Lines should not have trailing whitespace (trailing_whitespace)
Warning APIProvider.swift:338 Lines should not have trailing whitespace (trailing_whitespace)
Warning APIProvider.swift:351 Lines should not have trailing whitespace (trailing_whitespace)
Warning APIProvider.swift:362 Lines should not have trailing whitespace (trailing_whitespace)
Warning APIProvider.swift:372 Lines should not have trailing whitespace (trailing_whitespace)
Warning APIProvider.swift:380 Lines should not have trailing whitespace (trailing_whitespace)
Warning APIProvider.swift:106 Parentheses are not needed when declaring closure arguments (unneeded_parentheses_in_closure_argument)

Generated by 🚫 Danger Swift against f171bc2

Copy link
Owner

@AvdLee AvdLee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for your contribution! Unfortunately, I don't think this will solve the issue. It will make it impossible to run requests in parallel, which is a decrease in performance.

Instead, we should fix this inside JWTRequestsAuthenticator. In hindsight, without the use of an actor since we're still supporting non-async calls.

To do this, we can add a lock queue around all logic inside createToken(). A serial DispatchQueue with a custom label will do for now since it's unlikely to result in performance issues.

Let me know if you're able to pick this up!

@nickasd
Copy link
Contributor Author

nickasd commented Sep 23, 2023

Thanks a lot for your contribution! Unfortunately, I don't think this will solve the issue. It will make it impossible to run requests in parallel, which is a decrease in performance.

Thanks for checking. Are you sure that requests cannot run in parallel? I just tried changing ApiProvider.swift by replacing this part

public func request<T>(_ endpoint: Request<T>) async throws -> T where T: Decodable {
    try await withCheckedThrowingContinuation { continuation in
        request(endpoint, completion: continuation.resume(with:))
    }
}

with this

public func request<T>(_ endpoint: Request<T>) async throws -> T where T: Decodable {
    return try await withCheckedThrowingContinuation { continuation in
        print("start \(endpoint.path)")
        request(endpoint) { result in
            print("end \(endpoint.path)")
            continuation.resume(with: result)
        }
    }
}

When running my app, which loads different resources at the same time, I get this output:

start /v1/apps/1516126920/appStoreVersions
end /v1/apps/1516126920/appStoreVersions
start /v1/appStoreVersions/a10abbf3-d8c1-41c5-b8d6-5584d6233aee/appStoreVersionLocalizations
start /v1/appStoreVersions/b7f6beb9-b1eb-4a4e-be81-1b8cbef43a27/appStoreVersionLocalizations
end /v1/appStoreVersions/b7f6beb9-b1eb-4a4e-be81-1b8cbef43a27/appStoreVersionLocalizations
start /v1/appStoreVersionLocalizations/d9b5cd20-5985-4173-a539-c5fd30486c9c/appPreviewSets
start /v1/appStoreVersionLocalizations/5d785f0d-3a6b-4239-b92c-0e6fc2c6b385/appPreviewSets
start /v1/appStoreVersionLocalizations/ddc0db09-6a40-4d1b-ad36-80d3b57b1333/appPreviewSets
start /v1/appStoreVersionLocalizations/52e7c3e9-0189-4aec-a3ca-02f9a2b375eb/appPreviewSets
start /v1/appStoreVersionLocalizations/6d90d4e0-2bc6-4403-ae8d-cc80850d87e4/appPreviewSets
end /v1/appStoreVersions/a10abbf3-d8c1-41c5-b8d6-5584d6233aee/appStoreVersionLocalizations
start /v1/appStoreVersionLocalizations/15e208e5-451a-4e88-9e1d-09cfb68bc416/appPreviewSets
start /v1/appStoreVersionLocalizations/1aa0bcda-4d55-4c87-8b5a-8d8582a1578d/appPreviewSets
start /v1/appStoreVersionLocalizations/54d2e544-6b35-47f5-b49a-a12694dd0a95/appPreviewSets
start /v1/appStoreVersionLocalizations/f24c988e-0551-4bfb-acee-7bc0968a40e7/appPreviewSets
start /v1/appStoreVersionLocalizations/326f5d17-97de-4a95-9b81-be06499dda6e/appPreviewSets
end /v1/appStoreVersionLocalizations/ddc0db09-6a40-4d1b-ad36-80d3b57b1333/appPreviewSets
end /v1/appStoreVersionLocalizations/6d90d4e0-2bc6-4403-ae8d-cc80850d87e4/appPreviewSets
end /v1/appStoreVersionLocalizations/52e7c3e9-0189-4aec-a3ca-02f9a2b375eb/appPreviewSets
end /v1/appStoreVersionLocalizations/15e208e5-451a-4e88-9e1d-09cfb68bc416/appPreviewSets
end /v1/appStoreVersionLocalizations/54d2e544-6b35-47f5-b49a-a12694dd0a95/appPreviewSets
end /v1/appStoreVersionLocalizations/d9b5cd20-5985-4173-a539-c5fd30486c9c/appPreviewSets
start /v1/appPreviewSets/2c2825e5-375f-4d8f-8664-0239f0552a87/appPreviews
end /v1/appStoreVersionLocalizations/5d785f0d-3a6b-4239-b92c-0e6fc2c6b385/appPreviewSets
end /v1/appStoreVersionLocalizations/f24c988e-0551-4bfb-acee-7bc0968a40e7/appPreviewSets
end /v1/appStoreVersionLocalizations/1aa0bcda-4d55-4c87-8b5a-8d8582a1578d/appPreviewSets
end /v1/appStoreVersionLocalizations/326f5d17-97de-4a95-9b81-be06499dda6e/appPreviewSets
end /v1/appPreviewSets/2c2825e5-375f-4d8f-8664-0239f0552a87/appPreviews

Isn't this a sign that requests run in parallel?

@mathiasemil
Copy link
Contributor

mathiasemil commented Sep 27, 2023

Thanks a lot for your contribution! Unfortunately, I don't think this will solve the issue. It will make it impossible to run requests in parallel, which is a decrease in performance.

Thanks for checking. Are you sure that requests cannot run in parallel? I just tried changing ApiProvider.swift by replacing this part

public func request<T>(_ endpoint: Request<T>) async throws -> T where T: Decodable {
    try await withCheckedThrowingContinuation { continuation in
        request(endpoint, completion: continuation.resume(with:))
    }
}

with this

public func request<T>(_ endpoint: Request<T>) async throws -> T where T: Decodable {
    return try await withCheckedThrowingContinuation { continuation in
        print("start \(endpoint.path)")
        request(endpoint) { result in
            print("end \(endpoint.path)")
            continuation.resume(with: result)
        }
    }
}

When running my app, which loads different resources at the same time, I get this output:

start /v1/apps/1516126920/appStoreVersions
end /v1/apps/1516126920/appStoreVersions
start /v1/appStoreVersions/a10abbf3-d8c1-41c5-b8d6-5584d6233aee/appStoreVersionLocalizations
start /v1/appStoreVersions/b7f6beb9-b1eb-4a4e-be81-1b8cbef43a27/appStoreVersionLocalizations
end /v1/appStoreVersions/b7f6beb9-b1eb-4a4e-be81-1b8cbef43a27/appStoreVersionLocalizations
start /v1/appStoreVersionLocalizations/d9b5cd20-5985-4173-a539-c5fd30486c9c/appPreviewSets
start /v1/appStoreVersionLocalizations/5d785f0d-3a6b-4239-b92c-0e6fc2c6b385/appPreviewSets
start /v1/appStoreVersionLocalizations/ddc0db09-6a40-4d1b-ad36-80d3b57b1333/appPreviewSets
start /v1/appStoreVersionLocalizations/52e7c3e9-0189-4aec-a3ca-02f9a2b375eb/appPreviewSets
start /v1/appStoreVersionLocalizations/6d90d4e0-2bc6-4403-ae8d-cc80850d87e4/appPreviewSets
end /v1/appStoreVersions/a10abbf3-d8c1-41c5-b8d6-5584d6233aee/appStoreVersionLocalizations
start /v1/appStoreVersionLocalizations/15e208e5-451a-4e88-9e1d-09cfb68bc416/appPreviewSets
start /v1/appStoreVersionLocalizations/1aa0bcda-4d55-4c87-8b5a-8d8582a1578d/appPreviewSets
start /v1/appStoreVersionLocalizations/54d2e544-6b35-47f5-b49a-a12694dd0a95/appPreviewSets
start /v1/appStoreVersionLocalizations/f24c988e-0551-4bfb-acee-7bc0968a40e7/appPreviewSets
start /v1/appStoreVersionLocalizations/326f5d17-97de-4a95-9b81-be06499dda6e/appPreviewSets
end /v1/appStoreVersionLocalizations/ddc0db09-6a40-4d1b-ad36-80d3b57b1333/appPreviewSets
end /v1/appStoreVersionLocalizations/6d90d4e0-2bc6-4403-ae8d-cc80850d87e4/appPreviewSets
end /v1/appStoreVersionLocalizations/52e7c3e9-0189-4aec-a3ca-02f9a2b375eb/appPreviewSets
end /v1/appStoreVersionLocalizations/15e208e5-451a-4e88-9e1d-09cfb68bc416/appPreviewSets
end /v1/appStoreVersionLocalizations/54d2e544-6b35-47f5-b49a-a12694dd0a95/appPreviewSets
end /v1/appStoreVersionLocalizations/d9b5cd20-5985-4173-a539-c5fd30486c9c/appPreviewSets
start /v1/appPreviewSets/2c2825e5-375f-4d8f-8664-0239f0552a87/appPreviews
end /v1/appStoreVersionLocalizations/5d785f0d-3a6b-4239-b92c-0e6fc2c6b385/appPreviewSets
end /v1/appStoreVersionLocalizations/f24c988e-0551-4bfb-acee-7bc0968a40e7/appPreviewSets
end /v1/appStoreVersionLocalizations/1aa0bcda-4d55-4c87-8b5a-8d8582a1578d/appPreviewSets
end /v1/appStoreVersionLocalizations/326f5d17-97de-4a95-9b81-be06499dda6e/appPreviewSets
end /v1/appPreviewSets/2c2825e5-375f-4d8f-8664-0239f0552a87/appPreviews

Isn't this a sign that requests run in parallel?

Hi @nickasd 👋🏼 I'll chime in here. If ApiProvider is an actor and you make all the requests from a single ApiProvider instance I will expect them to be executed sequentially.
Even if that's not the case I will prefer ApiProvider to not be an actor to continue support for non-async calls.

To follow @AvdLee's suggestion I think we can fix the underlying issue in JWTRequestsAuthenticator with the following changes:

private let dispatchQueue = DispatchQueue(label: "JWTRequestsAuthenticator", attributes: .concurrent)

private func createToken() throws -> JWT.Token {
        try dispatchQueue.sync(flags: .barrier) {
            if let cachedToken = cachedToken, !cachedToken.isExpired {
                return cachedToken
            }

            let token = try jwtCreator.signedToken(using: apiConfiguration.privateKey)
            cachedToken = token
            return token
        }
}

I can make a PR to make this easier to read.

Let me know what you think 🙏🏼

@nickasd
Copy link
Contributor Author

nickasd commented Sep 28, 2023

Hey @mathiasemil, that should work as well. But why use a concurrent queue if you only submit barriers to it, and not use a standard serial queue?

I was just thinking, since what we want to protect here is concurrent read and write to the cachedToken property, can't we instead assign cachedToken once at the beginning, e.g. in apiConfiguration.didSet?

@mathiasemil
Copy link
Contributor

Hey @mathiasemil, that should work as well. But why use a concurrent queue if you only submit barriers to it, and not use a standard serial queue?

I was just thinking, since what we want to protect here is concurrent read and write to the cachedToken property, can't we instead assign cachedToken once at the beginning, e.g. in apiConfiguration.didSet?

I guess in this case there is no reason to not use a serial queue instead. So that sounds like a better approach 👍🏼

@nickasd
Copy link
Contributor Author

nickasd commented Sep 28, 2023

I just realized now that it also checks for !cachedToken.isExpired, so obviously the token cannot be created once at the beginning.

I'll update the PR.

@nickasd
Copy link
Contributor Author

nickasd commented Sep 28, 2023

Continued in #246.

@nickasd nickasd closed this Sep 28, 2023
@nickasd nickasd deleted the apiprovider-actor branch September 28, 2023 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants