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
Conversation
dispatchGroup.wait() | ||
} | ||
private func retrySynchronizedTokenRefresh() -> AnyPublisher<AccessToken, Error> { | ||
tokenProvider.invalidateAccessToken() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be deffered
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be deffered
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be deferred
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deferred can be removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
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 theaccessToken
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.receive(on: DispatchQueue.global())
andreceive(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.AuthenticationTokenProvidable
API contractreissueAccessToken()
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.