Skip to content

Commit 7895478

Browse files
FirebaseInstallations: API requests backoff (#6232)
* FirebaseInstallations: API requests backoff (#6207) * FIRInstallationsBackoffController introduced * FIRCurrentDateProvider introduced * FIRInstallationsBackoffControllerTests * style * FIRInstallationsBackoffController implementation * WIP: tests * Response to backoff event mapping * FIRInstallationsIDController: integration with FIRInstallationsBackoffController * Remove not existing error code. * FIRInstallationsIDController: integration with FIRInstallationsBackoffController * FIRInstallationsIDController: depend on FIRInstallationsBackoffControllerProtocol * FIRInstallationsIDControllerTests: supplement tests with backoff validation * TODOs * TODO * API docs * Header import fixed * FIS backoff adjustments and tests (#6230) * Maximum backoff interval 30 min * Errors adjusted * Tests updated * Exclude 401 and 404 from backoff as autorecoverable * comments * FIS 1.6.0 changelog (#6234) * Change log version fixed
1 parent eabe8d4 commit 7895478

14 files changed

+877
-50
lines changed

FirebaseInstallations/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
# v1.5.1 -- Unreleased
1+
# v1.7.0 -- Unreleased
22
- [changed] Use ephemeral `NSURLSession` to prevent caching of request/response. (#6226)
3+
- [changed] Backoff added for some error to prevent unnecessary API requests. (#6232)
34

45
# v1.5.0 -- M75
56
- [changed] Functionally neutral source reorganization. (#5832)

FirebaseInstallations/Source/Library/Errors/FIRInstallationsErrorUtil.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#import "FirebaseInstallations/Source/Library/Public/FirebaseInstallations/FIRInstallationsErrors.h"
2020

2121
@class FIRInstallationsHTTPError;
22+
@class FBLPromise<ResultType>;
2223

2324
NS_ASSUME_NONNULL_BEGIN
2425

@@ -45,12 +46,16 @@ void FIRInstallationsItemSetErrorToPointer(NSError *error, NSError **pointer);
4546
data:(nullable NSData *)data;
4647
+ (BOOL)isAPIError:(NSError *)error withHTTPCode:(NSInteger)HTTPCode;
4748

49+
+ (NSError *)backoffIntervalWaitError;
50+
4851
/**
4952
* Returns the passed error if it is already in the public domain or a new error with the passed
5053
* error at `NSUnderlyingErrorKey`.
5154
*/
5255
+ (NSError *)publicDomainErrorWithError:(NSError *)error;
5356

57+
+ (FBLPromise *)rejectedPromiseWithError:(NSError *)error;
58+
5459
@end
5560

5661
NS_ASSUME_NONNULL_END

FirebaseInstallations/Source/Library/Errors/FIRInstallationsErrorUtil.m

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818

1919
#import "FirebaseInstallations/Source/Library/Errors/FIRInstallationsHTTPError.h"
2020

21+
#if __has_include(<FBLPromises/FBLPromises.h>)
22+
#import <FBLPromises/FBLPromises.h>
23+
#else
24+
#import "FBLPromises.h"
25+
#endif
26+
2127
NSString *const kFirebaseInstallationsErrorDomain = @"com.firebase.installations";
2228

2329
void FIRInstallationsItemSetErrorToPointer(NSError *error, NSError **pointer) {
@@ -101,6 +107,12 @@ + (NSError *)networkErrorWithError:(NSError *)error {
101107
underlyingError:error];
102108
}
103109

110+
+ (NSError *)backoffIntervalWaitError {
111+
return [self installationsErrorWithCode:FIRInstallationsErrorCodeServerUnreachable
112+
failureReason:@"Too many server requests."
113+
underlyingError:nil];
114+
}
115+
104116
+ (NSError *)publicDomainErrorWithError:(NSError *)error {
105117
if ([error.domain isEqualToString:kFirebaseInstallationsErrorDomain]) {
106118
return error;
@@ -121,4 +133,10 @@ + (NSError *)installationsErrorWithCode:(FIRInstallationsErrorCode)code
121133
return [NSError errorWithDomain:kFirebaseInstallationsErrorDomain code:code userInfo:userInfo];
122134
}
123135

136+
+ (FBLPromise *)rejectedPromiseWithError:(NSError *)error {
137+
FBLPromise *rejectedPromise = [FBLPromise pendingPromise];
138+
[rejectedPromise reject:error];
139+
return rejectedPromise;
140+
}
141+
124142
@end

FirebaseInstallations/Source/Library/Errors/FIRInstallationsHTTPError.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ typedef NS_ENUM(NSInteger, FIRInstallationsHTTPCodes) {
4141
typedef NS_ENUM(NSInteger, FIRInstallationsRegistrationHTTPCode) {
4242
FIRInstallationsRegistrationHTTPCodeSuccess = 201,
4343
FIRInstallationsRegistrationHTTPCodeInvalidArgument = 400,
44-
FIRInstallationsRegistrationHTTPCodeInvalidAPIKey = 401,
4544
FIRInstallationsRegistrationHTTPCodeAPIKeyToProjectIDMismatch = 403,
4645
FIRInstallationsRegistrationHTTPCodeProjectNotFound = 404,
4746
FIRInstallationsRegistrationHTTPCodeTooManyRequests = 429,

FirebaseInstallations/Source/Library/InstallationsAPI/FIRInstallationsAPIService.m

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
2828
#import "FirebaseInstallations/Source/Library/Errors/FIRInstallationsErrorUtil.h"
29+
#import "FirebaseInstallations/Source/Library/Errors/FIRInstallationsHTTPError.h"
2930
#import "FirebaseInstallations/Source/Library/FIRInstallationsLogger.h"
3031
#import "FirebaseInstallations/Source/Library/InstallationsAPI/FIRInstallationsItem+RegisterInstallationAPI.h"
3132

@@ -333,7 +334,8 @@ - (instancetype)initWithURLSession:(NSURLSession *)URLSession
333334
return [FBLPromise attempts:1
334335
delay:1
335336
condition:^BOOL(NSInteger remainingAttempts, NSError *_Nonnull error) {
336-
return [FIRInstallationsErrorUtil isAPIError:error withHTTPCode:500];
337+
return [FIRInstallationsErrorUtil isAPIError:error
338+
withHTTPCode:FIRInstallationsHTTPCodesServerInternalError];
337339
}
338340
retry:^id _Nullable {
339341
return [self URLRequestPromise:request];
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2020 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+
17+
#import <Foundation/Foundation.h>
18+
19+
NS_ASSUME_NONNULL_BEGIN
20+
21+
/** A block returning current date. */
22+
typedef NSDate *_Nonnull (^FIRCurrentDateProvider)(void);
23+
24+
/** The function returns a `FIRCurrentDateProvider` block that returns a real current date. */
25+
FIRCurrentDateProvider FIRRealCurrentDateProvider(void);
26+
27+
NS_ASSUME_NONNULL_END
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2020 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+
17+
#import "FirebaseInstallations/Source/Library/InstallationsIDController/FIRCurrentDateProvider.h"
18+
19+
FIRCurrentDateProvider FIRRealCurrentDateProvider(void) {
20+
return ^NSDate *(void) {
21+
return [NSDate date];
22+
};
23+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2020 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+
17+
#import <Foundation/Foundation.h>
18+
19+
#import "FirebaseInstallations/Source/Library/InstallationsIDController/FIRCurrentDateProvider.h"
20+
21+
NS_ASSUME_NONNULL_BEGIN
22+
23+
typedef NS_ENUM(NSInteger, FIRInstallationsBackoffEvent) {
24+
FIRInstallationsBackoffEventSuccess,
25+
FIRInstallationsBackoffEventRecoverableFailure,
26+
FIRInstallationsBackoffEventUnrecoverableFailure
27+
};
28+
29+
/** The protocol defines API for a class that encapsulates backoff logic that prevents the SDK from
30+
* sending unnecessary server requests. See API docs for the methods for more details. */
31+
32+
@protocol FIRInstallationsBackoffControllerProtocol <NSObject>
33+
34+
/** The client must call the method each time a protected server request succeeds of fails. It will
35+
* affect the `isNextRequestAllowed` method result for the current time, e.g. when 3 recoverable
36+
* errors were logged in a row, then `isNextRequestAllowed` will return `YES` only in `pow(2, 3)`
37+
* seconds. */
38+
- (void)registerEvent:(FIRInstallationsBackoffEvent)event;
39+
40+
/** Returns if sending a next protected is recommended based on the time and the sequence of logged
41+
* events and the current time. See also `registerEvent:`. */
42+
- (BOOL)isNextRequestAllowed;
43+
44+
@end
45+
46+
/** An implementation of `FIRInstallationsBackoffControllerProtocol` with exponential backoff for
47+
* recoverable errors and constant backoff for recoverable errors. */
48+
@interface FIRInstallationsBackoffController : NSObject <FIRInstallationsBackoffControllerProtocol>
49+
50+
- (instancetype)initWithCurrentDateProvider:(FIRCurrentDateProvider)currentDateProvider;
51+
52+
@end
53+
54+
NS_ASSUME_NONNULL_END
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright 2020 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+
17+
#import "FirebaseInstallations/Source/Library/InstallationsIDController/FIRInstallationsBackoffController.h"
18+
19+
static const NSTimeInterval k24Hours = 24 * 60 * 60;
20+
static const NSTimeInterval k30Minutes = 30 * 60;
21+
22+
/** The class represents `FIRInstallationsBackoffController` sate required to calculate next allowed
23+
request time. The properties of the class are intentionally immutable because changing them
24+
separately leads to an inconsistent state. */
25+
@interface FIRInstallationsBackoffEventData : NSObject
26+
27+
@property(nonatomic, readonly) FIRInstallationsBackoffEvent eventType;
28+
@property(nonatomic, readonly) NSDate *lastEventDate;
29+
@property(nonatomic, readonly) NSInteger eventCount;
30+
31+
@property(nonatomic, readonly) NSTimeInterval backoffTimeInterval;
32+
33+
@end
34+
35+
@implementation FIRInstallationsBackoffEventData
36+
37+
- (instancetype)initWithEvent:(FIRInstallationsBackoffEvent)eventType
38+
lastEventDate:(NSDate *)lastEventDate
39+
eventCount:(NSInteger)eventCount {
40+
self = [super init];
41+
if (self) {
42+
_eventType = eventType;
43+
_lastEventDate = lastEventDate;
44+
_eventCount = eventCount;
45+
46+
_backoffTimeInterval = [[self class] backoffTimeIntervalWithEvent:eventType
47+
eventCount:eventCount];
48+
}
49+
return self;
50+
}
51+
52+
+ (NSTimeInterval)backoffTimeIntervalWithEvent:(FIRInstallationsBackoffEvent)eventType
53+
eventCount:(NSInteger)eventCount {
54+
switch (eventType) {
55+
case FIRInstallationsBackoffEventSuccess:
56+
return 0;
57+
break;
58+
59+
case FIRInstallationsBackoffEventRecoverableFailure:
60+
return [self recoverableErrorBackoffTimeForAttemptNumber:eventCount];
61+
break;
62+
63+
case FIRInstallationsBackoffEventUnrecoverableFailure:
64+
return k24Hours;
65+
break;
66+
}
67+
}
68+
69+
+ (NSTimeInterval)recoverableErrorBackoffTimeForAttemptNumber:(NSInteger)attemptNumber {
70+
NSTimeInterval exponentialInterval = pow(2, attemptNumber) + [self randomMilliseconds];
71+
return MIN(exponentialInterval, k30Minutes);
72+
}
73+
74+
+ (NSTimeInterval)randomMilliseconds {
75+
int32_t random_millis = ABS(arc4random() % 1000);
76+
return (double)random_millis * 0.001;
77+
}
78+
79+
@end
80+
81+
@interface FIRInstallationsBackoffController ()
82+
83+
@property(nonatomic, readonly) FIRCurrentDateProvider currentDateProvider;
84+
85+
@property(nonatomic, nullable) FIRInstallationsBackoffEventData *lastEventData;
86+
87+
@end
88+
89+
@implementation FIRInstallationsBackoffController
90+
91+
- (instancetype)init {
92+
return [self initWithCurrentDateProvider:FIRRealCurrentDateProvider()];
93+
}
94+
95+
- (instancetype)initWithCurrentDateProvider:(FIRCurrentDateProvider)currentDateProvider {
96+
self = [super init];
97+
if (self) {
98+
_currentDateProvider = [currentDateProvider copy];
99+
}
100+
return self;
101+
}
102+
103+
- (BOOL)isNextRequestAllowed {
104+
@synchronized(self) {
105+
if (self.lastEventData == nil) {
106+
return YES;
107+
}
108+
109+
NSTimeInterval timeSinceLastEvent =
110+
[self.currentDateProvider() timeIntervalSinceDate:self.lastEventData.lastEventDate];
111+
return timeSinceLastEvent >= self.lastEventData.backoffTimeInterval;
112+
}
113+
}
114+
115+
- (void)registerEvent:(FIRInstallationsBackoffEvent)event {
116+
@synchronized(self) {
117+
// Event of the same type as was registered before.
118+
if (self.lastEventData && self.lastEventData.eventType == event) {
119+
self.lastEventData = [[FIRInstallationsBackoffEventData alloc]
120+
initWithEvent:event
121+
lastEventDate:self.currentDateProvider()
122+
eventCount:self.lastEventData.eventCount + 1];
123+
} else { // A different event.
124+
self.lastEventData =
125+
[[FIRInstallationsBackoffEventData alloc] initWithEvent:event
126+
lastEventDate:self.currentDateProvider()
127+
eventCount:1];
128+
}
129+
}
130+
}
131+
132+
@end

0 commit comments

Comments
 (0)