Skip to content

Commit 58e73ba

Browse files
App Check: error handling with backoff (#8798)
* AppCheck: backoff wrapper implementation draft (#8748) * FIRAppCheckBackoffWrapper draft * style * FIRAppCheckBackoffWrapper implementation draft * FIRAppCheckBackoffWrapperTests * 1d backoff implementation * Fix tests * WIP FIRAppCheckBackoffWrapperFake * FIRAppCheckBackoffWrapperFake implementation * FIRDeviceCheckProviderTests + backoff * TODOs * rename * Docs, typos, names and minor refactoring * App Check: error handling with backoff (#8794) * Exponential backoff with jitter * modify random to arc4random_uniform * Error handling WIP * Default backoff strategy for backend errors * Default error handler implementation and tests * Exponential backoff tests and fixes * Exponential becoff recovery tests * style * App Attest backoff integration and tests * More tests and style * FIRDeviceCheckProviderTests: default error handler tests * test fixes * FIRAppAttestProviderTests: backoff error handler tests * Providers tests: test backoff case * Better API docs * Remove `resetBackoff` * More docs and comment fixes * FIRAppAttestProviderTests fixes and cleanup * Higlight 503 error * comment fix * Changelog * More details in changelog
1 parent 9b194b8 commit 58e73ba

File tree

10 files changed

+1194
-59
lines changed

10 files changed

+1194
-59
lines changed

FirebaseAppCheck/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# v8.9.0 -- M106
2+
- [fixed] Improved error handling logic by minimizing amount of requests that are unlikely to succeed. (#8798)
3+
14
# v8.8.0 -- M105
25
- [added] Add support for bundle ID-based API Key Restrictions (#8678)
36

FirebaseAppCheck/Sources/AppAttestProvider/FIRAppAttestProvider.m

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestArtifactStorage.h"
3232
#import "FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h"
3333
#import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h"
34+
#import "FirebaseAppCheck/Sources/Core/Backoff/FIRAppCheckBackoffWrapper.h"
3435
#import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h"
3536

3637
#import "FirebaseAppCheck/Sources/Core/Utils/FIRAppCheckCryptoUtils.h"
@@ -107,6 +108,7 @@ @interface FIRAppAttestProvider ()
107108
@property(nonatomic, readonly) id<FIRAppAttestService> appAttestService;
108109
@property(nonatomic, readonly) id<FIRAppAttestKeyIDStorageProtocol> keyIDStorage;
109110
@property(nonatomic, readonly) id<FIRAppAttestArtifactStorageProtocol> artifactStorage;
111+
@property(nonatomic, readonly) id<FIRAppCheckBackoffWrapperProtocol> backoffWrapper;
110112

111113
@property(nonatomic, nullable) FBLPromise<FIRAppCheckToken *> *ongoingGetTokenOperation;
112114

@@ -119,13 +121,15 @@ @implementation FIRAppAttestProvider
119121
- (instancetype)initWithAppAttestService:(id<FIRAppAttestService>)appAttestService
120122
APIService:(id<FIRAppAttestAPIServiceProtocol>)APIService
121123
keyIDStorage:(id<FIRAppAttestKeyIDStorageProtocol>)keyIDStorage
122-
artifactStorage:(id<FIRAppAttestArtifactStorageProtocol>)artifactStorage {
124+
artifactStorage:(id<FIRAppAttestArtifactStorageProtocol>)artifactStorage
125+
backoffWrapper:(id<FIRAppCheckBackoffWrapperProtocol>)backoffWrapper {
123126
self = [super init];
124127
if (self) {
125128
_appAttestService = appAttestService;
126129
_APIService = APIService;
127130
_keyIDStorage = keyIDStorage;
128131
_artifactStorage = artifactStorage;
132+
_backoffWrapper = backoffWrapper;
129133
_queue = dispatch_queue_create("com.firebase.FIRAppAttestProvider", DISPATCH_QUEUE_SERIAL);
130134
}
131135
return self;
@@ -155,10 +159,13 @@ - (nullable instancetype)initWithApp:(FIRApp *)app {
155159
appID:app.options.googleAppID
156160
accessGroup:app.options.appGroupID];
157161

162+
FIRAppCheckBackoffWrapper *backoffWrapper = [[FIRAppCheckBackoffWrapper alloc] init];
163+
158164
return [self initWithAppAttestService:DCAppAttestService.sharedService
159165
APIService:appAttestAPIService
160166
keyIDStorage:keyIDStorage
161-
artifactStorage:artifactStorage];
167+
artifactStorage:artifactStorage
168+
backoffWrapper:backoffWrapper];
162169
#else // FIR_APP_ATTEST_SUPPORTED_TARGETS
163170
return nil;
164171
#endif // FIR_APP_ATTEST_SUPPORTED_TARGETS
@@ -185,7 +192,7 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_
185192
// Kick off a new handshake sequence only when there is not an ongoing
186193
// handshake to avoid race conditions.
187194
self.ongoingGetTokenOperation =
188-
[self createGetTokenSequencePromise]
195+
[self createGetTokenSequenceWithBackoffPromise]
189196

190197
// Release the ongoing operation promise on completion.
191198
.then(^FIRAppCheckToken *(FIRAppCheckToken *token) {
@@ -201,6 +208,14 @@ - (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, NSError *_
201208
}];
202209
}
203210

211+
- (FBLPromise<FIRAppCheckToken *> *)createGetTokenSequenceWithBackoffPromise {
212+
return [self.backoffWrapper
213+
applyBackoffToOperation:^FBLPromise *_Nonnull {
214+
return [self createGetTokenSequencePromise];
215+
}
216+
errorHandler:[self.backoffWrapper defaultAppCheckProviderErrorHandler]];
217+
}
218+
204219
- (FBLPromise<FIRAppCheckToken *> *)createGetTokenSequencePromise {
205220
// Check attestation state to decide on the next steps.
206221
return [self attestationState].thenOn(self.queue, ^id(FIRAppAttestProviderState *attestState) {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
17+
#import <Foundation/Foundation.h>
18+
19+
@class FBLPromise<ValueType>;
20+
21+
NS_ASSUME_NONNULL_BEGIN
22+
23+
/// Backoff type. Backoff interval calculation depends on the type.
24+
typedef NS_ENUM(NSUInteger, FIRAppCheckBackoffType) {
25+
/// No backoff. Another retry is allowed straight away.
26+
FIRAppCheckBackoffTypeNone,
27+
28+
/// Next retry will be allowed in 1 day (24 hours) after the failure.
29+
FIRAppCheckBackoffType1Day,
30+
31+
/// A small backoff interval that exponentially increases after each consequent failure.
32+
FIRAppCheckBackoffTypeExponential
33+
};
34+
35+
/// Creates a promise for an operation to apply the backoff to.
36+
typedef FBLPromise *_Nonnull (^FIRAppCheckBackoffOperationProvider)(void);
37+
38+
/// Converts an error to a backoff type.
39+
typedef FIRAppCheckBackoffType (^FIRAppCheckBackoffErrorHandler)(NSError *error);
40+
41+
/// A block returning a date. Is used instead of `+[NSDate date]` for better testability of logic
42+
/// dependent on the current time.
43+
typedef NSDate *_Nonnull (^FIRAppCheckDateProvider)(void);
44+
45+
/// Defines API for an object that conditionally applies backoff to a given operation based on the
46+
/// history of previous operation failures.
47+
@protocol FIRAppCheckBackoffWrapperProtocol <NSObject>
48+
49+
/// Conditionally applies backoff to the given operation.
50+
/// @param operationProvider A block that returns a new promise. The block will be called only when
51+
/// the operation is allowed.
52+
/// NOTE: We cannot accept just a promise because the operation will be started once the
53+
/// promise has been instantiated, so we need to have a way to instantiate the promise only
54+
/// when the operation is good to go. The provider block is the way we use.
55+
/// @param errorHandler A block that receives an operation error as an input and returns the
56+
/// appropriate backoff type. `defaultErrorHandler` provides a default implementation for Firebase
57+
/// services.
58+
/// @return A promise that is either:
59+
/// - a promise returned by the promise provider if no backoff is required
60+
/// - rejected if the backoff is needed
61+
- (FBLPromise *)applyBackoffToOperation:(FIRAppCheckBackoffOperationProvider)operationProvider
62+
errorHandler:(FIRAppCheckBackoffErrorHandler)errorHandler;
63+
64+
/// The default Firebase services error handler. It keeps track of network errors and
65+
/// `FIRAppCheckHTTPError.HTTPResponse.statusCode.statusCode` value to return the appropriate
66+
/// backoff type for the standard Firebase App Check backend response codes.
67+
- (FIRAppCheckBackoffErrorHandler)defaultAppCheckProviderErrorHandler;
68+
69+
@end
70+
71+
/// Provides a backoff implementation. Keeps track of the operation successes and failures to either
72+
/// create and perform the operation promise or fails with a backoff error when the backoff is
73+
/// needed.
74+
@interface FIRAppCheckBackoffWrapper : NSObject <FIRAppCheckBackoffWrapperProtocol>
75+
76+
/// Initializes the wrapper with `+[FIRAppCheckBackoffWrapper currentDateProvider]`.
77+
- (instancetype)init;
78+
79+
- (instancetype)initWithDateProvider:(FIRAppCheckDateProvider)dateProvider
80+
NS_DESIGNATED_INITIALIZER;
81+
82+
/// A date provider that returns `+[NSDate date]`.
83+
+ (FIRAppCheckDateProvider)currentDateProvider;
84+
85+
@end
86+
87+
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)