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

Overhaul Refresh Token Retry Mechanism #4

Merged
merged 3 commits into from Apr 28, 2020

Conversation

erenkabakci
Copy link
Owner

@erenkabakci erenkabakci commented Apr 28, 2020

Changing the retry token mechanism and dropping the usage of DispatchGroup. Currently refresh token requests can run on parallel and will refresh tokens for themselves respectively but only the last valid one will be used in the next call. So we don't prevent firing them anymore but defer the usage of the accessToken

  • This is actually a setback in terms of atomic refresh token request execution, but the consumer app showed that the dispatchGroup usage was causing a deadlock.
  • refreshTriggeringErrors is now a [fusion.NetworkError] rather than [Error]. This was a poor designing since fusion can never evaluate based on consumer app's custom error. The consumer should provide an appropriate fusion error to trigger the refreshing mechanism.
  • Explicit thread management by adding receive(on: DispatchQueue.global()) and receive(on: DispatchQueue.main) to every execution of the API call to ensure that returning threads are not UI blocking and delivery still happens on the main thread.
  • Changes in the AuthenticationTokenProvidable API contract reissueAccessToken() is not returning a publisher.

Known issue #5 : One test is disabled due to an unknown (possibly threading) issue causing multiple other tests fail. This should ideally be fixed and still using EntwineTest for better observation of the stream.

@emanuelschmoczer emanuelschmoczer merged commit 8512cd2 into master Apr 28, 2020
dispatchGroup.wait()
}
private func retrySynchronizedTokenRefresh() -> AnyPublisher<AccessToken, Error> {
tokenProvider.invalidateAccessToken()
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be deffered

Copy link
Owner Author

Choose a reason for hiding this comment

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

the consumer of this publisher is already deferred

@@ -32,7 +32,7 @@ public protocol SessionPublisherProtocol: AnyObject {
extension URLSession: SessionPublisherProtocol {
public func dataTaskPublisher(for request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), Error> {
self.dataTaskPublisher(for: request)
.receive(on: DispatchQueue.main)
.subscribe(on: DispatchQueue.global())
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think networking framework should decide where to subscribe and publish it's changes

Copy link
Owner Author

Choose a reason for hiding this comment

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

It's a design decision that I took while creating the framework. For the consumer it doesn't change anything actually but I wanted fusion to provide a hassle-free development for the consumer. Therefore all the executions happen on the background thread and the deliveries happen on the main queue.

If at any point a consumer wants to use a different thread, they are free to use subscribe or receive

authenticationQueue.sync {
currentAccessToken = self.tokenProvider.accessToken.value
func appendTokenAndExecute(accessToken: AccessToken) -> AnyPublisher<T, Error> {
urlRequest.setValue(self.configuration.authorizationHeaderScheme.rawValue + accessToken, forHTTPHeaderField: "Authorization")
Copy link
Contributor

Choose a reason for hiding this comment

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

should be deffered

Copy link
Owner Author

Choose a reason for hiding this comment

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

again, the consumer of this is deferred already. There is no direct access to this publisher without any defer mechanism.

func appendTokenAndExecute(accessToken: AccessToken) -> AnyPublisher<T, Error> {
urlRequest.setValue(self.configuration.authorizationHeaderScheme.rawValue + accessToken, forHTTPHeaderField: "Authorization")
return super.execute(urlRequest: urlRequest)
.subscribe(on: DispatchQueue.global())
Copy link
Contributor

Choose a reason for hiding this comment

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

same here, don't think it's responsibility of the framework itself

Copy link
Owner Author

Choose a reason for hiding this comment

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

It's a design decision that I took while creating the framework. For the consumer it doesn't change anything actually but I wanted fusion to provide a hassle-free development for the consumer. Therefore all the executions happen on the background thread and the deliveries happen on the main queue.

If at any point a consumer wants to use a different thread, they are free to use subscribe or receive

.eraseToAnyPublisher()
}

return Deferred {
Copy link
Contributor

Choose a reason for hiding this comment

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

this deferred can be removed

Copy link
Owner Author

Choose a reason for hiding this comment

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

This, I am not sure since this was sort of the fixing point for the racing refrehTokens. I will let @emanuelschmoczer elaborate on this.

authenticationQueue.sync {
currentAccessToken = self.tokenProvider.accessToken.value
func appendTokenAndExecute(accessToken: AccessToken) -> AnyPublisher<Void, Error> {
urlRequest.setValue(self.configuration.authorizationHeaderScheme.rawValue + accessToken, forHTTPHeaderField: "Authorization")
Copy link
Contributor

Choose a reason for hiding this comment

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

should be deferred

Copy link
Owner Author

Choose a reason for hiding this comment

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

Consumer is deferred already.

guard let accessToken = currentAccessToken else {
return Fail<Void, Error>(error: NetworkError.unauthorized).eraseToAnyPublisher()
return Deferred {
self.tokenProvider.accessToken
Copy link
Contributor

Choose a reason for hiding this comment

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

deferred can be removed

Copy link
Owner Author

Choose a reason for hiding this comment

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

Comment on lines +111 to +123
return appendTokenAndExecute(accessToken: accessToken)
}.eraseToAnyPublisher()
}
.timeout(10, scheduler: DispatchQueue.main, customError: { NetworkError.timeout })
.catch { [weak self] error -> AnyPublisher<Void, Error> in
guard let self = self else {
return Fail<Void, Error>(error: NetworkError.unknown).eraseToAnyPublisher()
}

if self.configuration.refreshTriggerErrors.contains(where: { return $0.reflectedString == error.reflectedString }){
return self.retrySynchronizedTokenRefresh()
.flatMap {
appendTokenAndExecute(accessToken: $0)
Copy link
Contributor

Choose a reason for hiding this comment

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

you have duplicated calls to appendTokenAndExecute in one function, which I think design issue

Copy link
Owner Author

Choose a reason for hiding this comment

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

yes, it looks weird indeed. This was a recursive function before, for the sake of readability and understandability we changed it to this. So the first appending happens when the request is executed for the first time, the second one is if it fails due to a 401. So it decorates the request and fires again. Should we go back to the recursion?

}
return Fail<Void, Error>(error: error).eraseToAnyPublisher()
}.eraseToAnyPublisher()
.receive(on: DispatchQueue.main)
Copy link
Contributor

Choose a reason for hiding this comment

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

should be responsibility of the consumer

Copy link
Owner Author

Choose a reason for hiding this comment

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

It's a design decision that I took while creating the framework. For the consumer it doesn't change anything actually but I wanted fusion to provide a hassle-free development for the consumer. Therefore all the executions happen on the background thread and the deliveries happen on the main queue.

If at any point a consumer wants to use a different thread, they are free to use subscribe or receive

promise(.failure(NetworkError.urlError(errorResponse)))
return Deferred {
Future<(data: Data, response: URLResponse), Error> { promise in
usleep(20)
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need this timeout? it doesn't make any sense to me and looks like a system design issue

Copy link
Owner Author

Choose a reason for hiding this comment

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

I put it just to simulate a slower execution in tests. It has nothing to do with the production code. I wanted to execute them slightly after each other. I guess it can be removed.

@ediazest ediazest mentioned this pull request Apr 29, 2020
erenkabakci added a commit that referenced this pull request Sep 24, 2020
@erenkabakci erenkabakci deleted the feature/reactiveRefreshToken branch November 30, 2020 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants