FIRAppCheckBackoffWrapper.m 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. /*
  2. * Copyright 2021 Google LLC
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "FirebaseAppCheck/Sources/Core/Backoff/FIRAppCheckBackoffWrapper.h"
  17. #if __has_include(<FBLPromises/FBLPromises.h>)
  18. #import <FBLPromises/FBLPromises.h>
  19. #else
  20. #import "FBLPromises.h"
  21. #endif
  22. #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h"
  23. #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckHTTPError.h"
  24. NS_ASSUME_NONNULL_BEGIN
  25. static NSTimeInterval const k24Hours = 24 * 60 * 60;
  26. /// Jitter coefficient 0.5 means that the backoff interval can be up to 50% longer.
  27. static double const kMaxJitterCoefficient = 0.5;
  28. /// Maximum exponential backoff interval.
  29. static double const kMaxExponentialBackoffInterval = 4 * 60 * 60; // 4 hours.
  30. /// A class representing an operation result with data required for the backoff calculation.
  31. @interface FIRAppCheckBackoffOperationFailure : NSObject
  32. /// The operation finish date.
  33. @property(nonatomic, readonly) NSDate *finishDate;
  34. /// The operation error.
  35. @property(nonatomic, readonly) NSError *error;
  36. /// A backoff type calculated based on the error.
  37. @property(nonatomic, readonly) FIRAppCheckBackoffType backoffType;
  38. /// Number of retries. Is 0 for the first attempt and incremented with each error. Is reset back to
  39. /// 0 on success.
  40. @property(nonatomic, readonly) NSInteger retryCount;
  41. /// Designated initializer.
  42. - (instancetype)initWithFinishDate:(NSDate *)finishDate
  43. error:(NSError *)error
  44. backoffType:(FIRAppCheckBackoffType)backoffType
  45. retryCount:(NSInteger)retryCount NS_DESIGNATED_INITIALIZER;
  46. - (instancetype)init NS_UNAVAILABLE;
  47. /// Creates a new result with incremented retryCount and specified error and backoff type.
  48. + (instancetype)nextRetryFailureWithFailure:
  49. (nullable FIRAppCheckBackoffOperationFailure *)previousFailure
  50. finishDate:(NSDate *)finishDate
  51. error:(NSError *)error
  52. backoffType:(FIRAppCheckBackoffType)backoffType;
  53. @end
  54. @implementation FIRAppCheckBackoffOperationFailure
  55. - (instancetype)initWithFinishDate:(NSDate *)finishDate
  56. error:(NSError *)error
  57. backoffType:(FIRAppCheckBackoffType)backoffType
  58. retryCount:(NSInteger)retryCount {
  59. self = [super init];
  60. if (self) {
  61. _finishDate = finishDate;
  62. _error = error;
  63. _retryCount = retryCount;
  64. _backoffType = backoffType;
  65. }
  66. return self;
  67. }
  68. + (instancetype)nextRetryFailureWithFailure:
  69. (nullable FIRAppCheckBackoffOperationFailure *)previousFailure
  70. finishDate:(NSDate *)finishDate
  71. error:(NSError *)error
  72. backoffType:(FIRAppCheckBackoffType)backoffType {
  73. NSInteger newRetryCount = previousFailure ? previousFailure.retryCount + 1 : 0;
  74. return [[self alloc] initWithFinishDate:finishDate
  75. error:error
  76. backoffType:backoffType
  77. retryCount:newRetryCount];
  78. }
  79. @end
  80. @interface FIRAppCheckBackoffWrapper ()
  81. /// Current date provider. Is used instead of `+[NSDate date]` for testability.
  82. @property(nonatomic, readonly) FIRAppCheckDateProvider dateProvider;
  83. /// Last operation result.
  84. @property(nonatomic, nullable) FIRAppCheckBackoffOperationFailure *lastFailure;
  85. @end
  86. @implementation FIRAppCheckBackoffWrapper
  87. - (instancetype)init {
  88. return [self initWithDateProvider:[FIRAppCheckBackoffWrapper currentDateProvider]];
  89. }
  90. - (instancetype)initWithDateProvider:(FIRAppCheckDateProvider)dateProvider {
  91. self = [super init];
  92. if (self) {
  93. _dateProvider = [dateProvider copy];
  94. }
  95. return self;
  96. }
  97. + (FIRAppCheckDateProvider)currentDateProvider {
  98. return ^NSDate *(void) {
  99. return [NSDate date];
  100. };
  101. }
  102. - (FBLPromise *)applyBackoffToOperation:(FIRAppCheckBackoffOperationProvider)operationProvider
  103. errorHandler:(FIRAppCheckBackoffErrorHandler)errorHandler {
  104. if (![self isNextOperationAllowed]) {
  105. // Backing off - skip the operation and return an error straight away.
  106. return [self promiseWithRetryDisallowedError:self.lastFailure.error];
  107. }
  108. __auto_type operationPromise = operationProvider();
  109. return operationPromise
  110. .thenOn([self queue],
  111. ^id(id result) {
  112. @synchronized(self) {
  113. // Reset failure on success.
  114. self.lastFailure = nil;
  115. }
  116. // Return the result.
  117. return result;
  118. })
  119. .recoverOn([self queue], ^NSError *(NSError *error) {
  120. @synchronized(self) {
  121. // Update the last failure to calculate the backoff.
  122. self.lastFailure =
  123. [FIRAppCheckBackoffOperationFailure nextRetryFailureWithFailure:self.lastFailure
  124. finishDate:self.dateProvider()
  125. error:error
  126. backoffType:errorHandler(error)];
  127. }
  128. // Re-throw the error.
  129. return error;
  130. });
  131. }
  132. #pragma mark - Private
  133. - (BOOL)isNextOperationAllowed {
  134. @synchronized(self) {
  135. if (self.lastFailure == nil) {
  136. // It is first attempt. Always allow it.
  137. return YES;
  138. }
  139. switch (self.lastFailure.backoffType) {
  140. case FIRAppCheckBackoffTypeNone:
  141. return YES;
  142. break;
  143. case FIRAppCheckBackoffType1Day:
  144. return [self hasTimeIntervalPassedSinceLastFailure:k24Hours];
  145. break;
  146. case FIRAppCheckBackoffTypeExponential:
  147. return [self hasTimeIntervalPassedSinceLastFailure:
  148. [self exponentialBackoffIntervalForFailure:self.lastFailure]];
  149. break;
  150. }
  151. }
  152. }
  153. - (BOOL)hasTimeIntervalPassedSinceLastFailure:(NSTimeInterval)timeInterval {
  154. NSDate *failureDate = self.lastFailure.finishDate;
  155. // Return YES if there has not been a failure yet.
  156. if (failureDate == nil) return YES;
  157. NSTimeInterval timeSinceFailure = [self.dateProvider() timeIntervalSinceDate:failureDate];
  158. return timeSinceFailure >= timeInterval;
  159. }
  160. - (FBLPromise *)promiseWithRetryDisallowedError:(NSError *)error {
  161. NSString *reason =
  162. [NSString stringWithFormat:@"Too many attempts. Underlying error: %@",
  163. error.localizedDescription ?: error.localizedFailureReason];
  164. NSError *retryDisallowedError = [FIRAppCheckErrorUtil errorWithFailureReason:reason];
  165. FBLPromise *rejectedPromise = [FBLPromise pendingPromise];
  166. [rejectedPromise reject:retryDisallowedError];
  167. return rejectedPromise;
  168. }
  169. - (dispatch_queue_t)queue {
  170. return dispatch_get_global_queue(QOS_CLASS_UTILITY, 0);
  171. }
  172. #pragma mark - Exponential backoff
  173. /// @return Exponential backoff interval with jitter. Jitter is needed to avoid all clients to retry
  174. /// at the same time after e.g. a backend outage.
  175. - (NSTimeInterval)exponentialBackoffIntervalForFailure:
  176. (FIRAppCheckBackoffOperationFailure *)failure {
  177. // Base exponential backoff interval.
  178. NSTimeInterval baseBackoff = pow(2, failure.retryCount);
  179. // Get a random number from 0 to 1.
  180. double maxRandom = 1000;
  181. double randomNumber = (double)arc4random_uniform((int32_t)maxRandom) / maxRandom;
  182. // A number from 1 to 1 + kMaxJitterCoefficient, e.g. from 1 to 1.5. Indicates how much the
  183. // backoff can be extended.
  184. double jitterCoefficient = 1 + randomNumber * kMaxJitterCoefficient;
  185. // Exponential backoff interval with jitter.
  186. NSTimeInterval backoffIntervalWithJitter = baseBackoff * jitterCoefficient;
  187. // Apply limit to the backoff interval.
  188. return MIN(backoffIntervalWithJitter, kMaxExponentialBackoffInterval);
  189. }
  190. #pragma mark - Error handling
  191. - (FIRAppCheckBackoffErrorHandler)defaultAppCheckProviderErrorHandler {
  192. return ^FIRAppCheckBackoffType(NSError *error) {
  193. FIRAppCheckHTTPError *HTTPError =
  194. [error isKindOfClass:[FIRAppCheckHTTPError class]] ? (FIRAppCheckHTTPError *)error : nil;
  195. if (HTTPError == nil) {
  196. // No backoff for attestation providers for non-backend (e.g. network) errors.
  197. return FIRAppCheckBackoffTypeNone;
  198. }
  199. NSInteger statusCode = HTTPError.HTTPResponse.statusCode;
  200. if (statusCode < 400) {
  201. // No backoff for codes before 400.
  202. return FIRAppCheckBackoffTypeNone;
  203. }
  204. if (statusCode == 400 || statusCode == 404) {
  205. // Firebase project misconfiguration. It will unlikely be fixed soon and often requires
  206. // another version of the app. Try again in 1 day.
  207. return FIRAppCheckBackoffType1Day;
  208. }
  209. if (statusCode == 403) {
  210. // Project may have been soft-deleted accidentally. There is a chance of timely recovery, so
  211. // try again later.
  212. return FIRAppCheckBackoffTypeExponential;
  213. }
  214. if (statusCode == 429) {
  215. // Too many requests. Try again in a while.
  216. return FIRAppCheckBackoffTypeExponential;
  217. }
  218. if (statusCode == 503) {
  219. // Server is overloaded. Try again in a while.
  220. return FIRAppCheckBackoffTypeExponential;
  221. }
  222. // For all other server error cases default to the exponential backoff.
  223. return FIRAppCheckBackoffTypeExponential;
  224. };
  225. }
  226. @end
  227. NS_ASSUME_NONNULL_END