Skip to content

Commit 6d8b572

Browse files
authored
Enable request limiter for CreateInstallation, GenerateAuthToken request calls. (#2003)
* Enable request limiter for CreateInstallation & GenerateAuthToken request calls. * Addressing Rayo's comments. * Addressing rayo's comments
1 parent 1443ac5 commit 6d8b572

File tree

6 files changed

+101
-29
lines changed

6 files changed

+101
-29
lines changed

firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import com.google.firebase.installations.remote.FirebaseInstallationServiceClient;
3535
import com.google.firebase.installations.remote.InstallationResponse;
3636
import com.google.firebase.installations.remote.TokenResult;
37-
import com.google.firebase.installations.time.SystemClock;
3837
import com.google.firebase.platforminfo.UserAgentPublisher;
3938
import java.io.IOException;
4039
import java.util.ArrayList;
@@ -133,7 +132,7 @@ public Thread newThread(Runnable r) {
133132
new FirebaseInstallationServiceClient(
134133
firebaseApp.getApplicationContext(), publisher, heartbeatInfo),
135134
new PersistedInstallation(firebaseApp),
136-
new Utils(SystemClock.getInstance()),
135+
Utils.getInstance(),
137136
new IidStore(firebaseApp),
138137
new RandomFidGenerator());
139138
}

firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ public enum Status {
3535
* may be corrected by retrying. We recommend exponential backoff when retrying requests.
3636
*/
3737
UNAVAILABLE,
38+
/**
39+
* Firebase servers have received too many requests in a short period of time from the client.
40+
*/
41+
TOO_MANY_REQUESTS,
3842
}
3943

4044
@NonNull private final Status status;

firebase-installations/src/main/java/com/google/firebase/installations/Utils.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import androidx.annotation.Nullable;
2020
import com.google.firebase.installations.local.PersistedInstallationEntry;
2121
import com.google.firebase.installations.time.Clock;
22+
import com.google.firebase.installations.time.SystemClock;
2223
import java.util.concurrent.TimeUnit;
2324
import java.util.regex.Pattern;
2425

@@ -31,11 +32,30 @@ public final class Utils {
3132
public static final long AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS = TimeUnit.HOURS.toSeconds(1);
3233
private static final String APP_ID_IDENTIFICATION_SUBSTRING = ":";
3334
private static final Pattern API_KEY_FORMAT = Pattern.compile("\\AA[\\w-]{38}\\z");
35+
private static Utils singleton;
3436
private final Clock clock;
3537

36-
Utils(Clock clock) {
38+
private Utils(Clock clock) {
3739
this.clock = clock;
3840
}
41+
42+
// Factory method that always returns the same Utils instance.
43+
public static Utils getInstance() {
44+
return getInstance(SystemClock.getInstance());
45+
}
46+
47+
/**
48+
* Returns an Utils instance. {@link Utils#getInstance()} defines the clock used. NOTE: If a Utils
49+
* instance has already been initialized, the parameter will be ignored and the existing instance
50+
* will be returned.
51+
*/
52+
public static Utils getInstance(Clock clock) {
53+
if (singleton == null) {
54+
singleton = new Utils(clock);
55+
}
56+
return singleton;
57+
}
58+
3959
/**
4060
* Checks if the FIS Auth token is expired or going to expire in next 1 hour {@link
4161
* #AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS}.

firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,15 @@ public class FirebaseInstallationServiceClient {
9797
private static final String SDK_VERSION_PREFIX = "a:";
9898

9999
private static final String FIS_TAG = "Firebase-Installations";
100+
private boolean shouldServerErrorRetry;
100101

101102
@VisibleForTesting
102103
static final String PARSING_EXPIRATION_TIME_ERROR_MESSAGE = "Invalid Expiration Timestamp.";
103104

104105
private final Context context;
105106
private final Provider<UserAgentPublisher> userAgentPublisher;
106107
private final Provider<HeartBeatInfo> heartbeatInfo;
108+
private final RequestLimiter requestLimiter;
107109

108110
public FirebaseInstallationServiceClient(
109111
@NonNull Context context,
@@ -112,6 +114,7 @@ public FirebaseInstallationServiceClient(
112114
this.context = context;
113115
this.userAgentPublisher = publisher;
114116
this.heartbeatInfo = heartbeatInfo;
117+
this.requestLimiter = new RequestLimiter();
115118
}
116119

117120
/**
@@ -140,10 +143,16 @@ public InstallationResponse createFirebaseInstallation(
140143
@NonNull String appId,
141144
@Nullable String iidToken)
142145
throws FirebaseInstallationsException {
146+
if (!requestLimiter.isRequestAllowed()) {
147+
throw new FirebaseInstallationsException(
148+
"Firebase Installations Service is unavailable. Please try again later.",
149+
Status.UNAVAILABLE);
150+
}
151+
143152
String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectID);
144-
int retryCount = 0;
145153
URL url = getFullyQualifiedRequestUri(resourceName);
146-
while (retryCount <= MAX_RETRIES) {
154+
for (int retryCount = 0; retryCount <= MAX_RETRIES; retryCount++) {
155+
147156
HttpURLConnection httpURLConnection = openHttpURLConnection(url, apiKey);
148157

149158
try {
@@ -158,15 +167,22 @@ public InstallationResponse createFirebaseInstallation(
158167
writeFIDCreateRequestBodyToOutputStream(httpURLConnection, fid, appId);
159168

160169
int httpResponseCode = httpURLConnection.getResponseCode();
170+
requestLimiter.setNextRequestTime(httpResponseCode);
161171

162-
if (httpResponseCode == 200) {
172+
if (isSuccessfulResponseCode(httpResponseCode)) {
163173
return readCreateResponse(httpURLConnection);
164174
}
165175

166176
logFisCommunicationError(httpURLConnection, appId, apiKey, projectID);
167177

168-
if (httpResponseCode == 429 || (httpResponseCode >= 500 && httpResponseCode < 600)) {
169-
retryCount++;
178+
if (httpResponseCode == 429) {
179+
throw new FirebaseInstallationsException(
180+
"Firebase servers have received too many requests from this client in a short "
181+
+ "period of time. Please try again later.",
182+
Status.TOO_MANY_REQUESTS);
183+
}
184+
185+
if (httpResponseCode >= 500 && httpResponseCode < 600) {
170186
continue;
171187
}
172188

@@ -175,7 +191,7 @@ public InstallationResponse createFirebaseInstallation(
175191
// Return empty installation response with BAD_CONFIG response code after max retries
176192
return InstallationResponse.builder().setResponseCode(ResponseCode.BAD_CONFIG).build();
177193
} catch (AssertionError | IOException ignored) {
178-
retryCount++;
194+
continue;
179195
} finally {
180196
httpURLConnection.disconnect();
181197
}
@@ -363,11 +379,17 @@ public TokenResult generateAuthToken(
363379
@NonNull String projectID,
364380
@NonNull String refreshToken)
365381
throws FirebaseInstallationsException {
382+
if (!requestLimiter.isRequestAllowed()) {
383+
throw new FirebaseInstallationsException(
384+
"Firebase Installations Service is unavailable. Please try again later.",
385+
Status.UNAVAILABLE);
386+
}
387+
366388
String resourceName =
367389
String.format(GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid);
368-
int retryCount = 0;
369390
URL url = getFullyQualifiedRequestUri(resourceName);
370-
while (retryCount <= MAX_RETRIES) {
391+
for (int retryCount = 0; retryCount <= MAX_RETRIES; retryCount++) {
392+
371393
HttpURLConnection httpURLConnection = openHttpURLConnection(url, apiKey);
372394
try {
373395
httpURLConnection.setRequestMethod("POST");
@@ -377,8 +399,9 @@ public TokenResult generateAuthToken(
377399
writeGenerateAuthTokenRequestBodyToOutputStream(httpURLConnection);
378400

379401
int httpResponseCode = httpURLConnection.getResponseCode();
402+
requestLimiter.setNextRequestTime(httpResponseCode);
380403

381-
if (httpResponseCode == 200) {
404+
if (isSuccessfulResponseCode(httpResponseCode)) {
382405
return readGenerateAuthTokenResponse(httpURLConnection);
383406
}
384407

@@ -388,8 +411,14 @@ public TokenResult generateAuthToken(
388411
return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.AUTH_ERROR).build();
389412
}
390413

391-
if (httpResponseCode == 429 || (httpResponseCode >= 500 && httpResponseCode < 600)) {
392-
retryCount++;
414+
if (httpResponseCode == 429) {
415+
throw new FirebaseInstallationsException(
416+
"Firebase servers have received too many requests from this client in a short "
417+
+ "period of time. Please try again later.",
418+
Status.TOO_MANY_REQUESTS);
419+
}
420+
421+
if (httpResponseCode >= 500 && httpResponseCode < 600) {
393422
continue;
394423
}
395424

@@ -398,7 +427,7 @@ public TokenResult generateAuthToken(
398427
return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.BAD_CONFIG).build();
399428
// TODO(b/166168291): Remove code duplication and clean up this class.
400429
} catch (AssertionError | IOException ignored) {
401-
retryCount++;
430+
continue;
402431
} finally {
403432
httpURLConnection.disconnect();
404433
}
@@ -408,6 +437,10 @@ public TokenResult generateAuthToken(
408437
Status.UNAVAILABLE);
409438
}
410439

440+
private static boolean isSuccessfulResponseCode(int responseCode) {
441+
return responseCode >= 200 && responseCode < 300;
442+
}
443+
411444
private static void logBadConfigError() {
412445
Log.e(
413446
FIS_TAG,

firebase-installations/src/main/java/com/google/firebase/installations/remote/RequestLimiter.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,15 @@ class RequestLimiter {
4242
this.utils = utils;
4343
}
4444

45+
RequestLimiter() {
46+
// Util class is injected to ease mocking & testing the system time.
47+
this.utils = Utils.getInstance();
48+
}
49+
4550
// Based on the response code, calculates the next request time to communicate with the FIS
4651
// servers.
4752
public synchronized void setNextRequestTime(int responseCode) {
48-
if (isSuccessful(responseCode)) {
53+
if (isSuccessfulOrRequiresNewFidCreation(responseCode)) {
4954
resetBackoffStrategy();
5055
return;
5156
}
@@ -77,10 +82,14 @@ private static boolean isRetryableError(int responseCode) {
7782
return responseCode == 429 || (responseCode >= 500 && responseCode < 600);
7883
}
7984

80-
// Response codes classified as success for FIS API. Read more on FIS response codes:
81-
// go/fis-api-error-code-classification.
82-
private static boolean isSuccessful(int responseCode) {
83-
return responseCode >= 200 && responseCode < 300;
85+
// 2xx Response codes are classified as success for FIS API. Also, FIS GenerateAuthToken endpoint
86+
// responds with 401 & 404 for auth config errors which requires clients to follow up with a
87+
// request to create a new FID. So, we don't limit the next requests for 401 & 404 response codes
88+
// as well. Read more on FIS response codes: go/fis-api-error-code-classification.
89+
private static boolean isSuccessfulOrRequiresNewFidCreation(int responseCode) {
90+
return ((responseCode >= 200 && responseCode < 300)
91+
|| responseCode == 401
92+
|| responseCode == 404);
8493
}
8594

8695
// Decides whether a network request to FIS servers is allowed to execute.

firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,7 @@ public void setUp() {
152152
persistedInstallation.clearForTesting();
153153

154154
fakeClock = new FakeClock(5000000L);
155-
utils = new Utils(fakeClock);
156-
155+
utils = Utils.getInstance(fakeClock);
157156
firebaseInstallations =
158157
new FirebaseInstallations(
159158
executor,
@@ -523,7 +522,9 @@ public void testGetId_expiredAuthTokenThrowsException_statusUpdated() throws Exc
523522
PersistedInstallationEntry.INSTANCE.withRegisteredFid(
524523
TEST_FID_1,
525524
TEST_REFRESH_TOKEN,
526-
utils.currentTimeInSecs(),
525+
utils.currentTimeInSecs()
526+
- TEST_TOKEN_EXPIRATION_TIMESTAMP
527+
+ TimeUnit.MINUTES.toSeconds(30),
527528
TEST_AUTH_TOKEN,
528529
TEST_TOKEN_EXPIRATION_TIMESTAMP));
529530

@@ -562,7 +563,10 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception {
562563
PersistedInstallationEntry.INSTANCE.withRegisteredFid(
563564
TEST_FID_1,
564565
TEST_REFRESH_TOKEN,
565-
utils.currentTimeInSecs(),
566+
// Set expiration time to 30 minutes from now (within refresh period)
567+
utils.currentTimeInSecs()
568+
- TEST_TOKEN_EXPIRATION_TIMESTAMP
569+
+ TimeUnit.MINUTES.toSeconds(30),
566570
TEST_AUTH_TOKEN,
567571
TEST_TOKEN_EXPIRATION_TIMESTAMP));
568572

@@ -582,9 +586,6 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception {
582586
String fid = onCompleteListener.await();
583587
assertWithMessage("getId Task failed").that(fid).isEqualTo(TEST_FID_1);
584588

585-
// Waiting for Task that registers FID on the FIS Servers
586-
executor.awaitTermination(500, TimeUnit.MILLISECONDS);
587-
588589
TestOnCompleteListener<InstallationTokenResult> onCompleteListener2 =
589590
new TestOnCompleteListener<>();
590591
Task<InstallationTokenResult> task = firebaseInstallations.getToken(false);
@@ -749,7 +750,10 @@ public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Ex
749750
PersistedInstallationEntry.INSTANCE.withRegisteredFid(
750751
TEST_FID_1,
751752
TEST_REFRESH_TOKEN,
752-
utils.currentTimeInSecs(),
753+
// Set expiration time to 30 minutes from now (within refresh period)
754+
utils.currentTimeInSecs()
755+
- TEST_TOKEN_EXPIRATION_TIMESTAMP
756+
+ TimeUnit.MINUTES.toSeconds(30),
753757
TEST_AUTH_TOKEN,
754758
TEST_TOKEN_EXPIRATION_TIMESTAMP));
755759

@@ -780,7 +784,10 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce(
780784
PersistedInstallationEntry.INSTANCE.withRegisteredFid(
781785
TEST_FID_1,
782786
TEST_REFRESH_TOKEN,
783-
utils.currentTimeInSecs(),
787+
// Set expiration time to 30 minutes from now (within refresh period)
788+
utils.currentTimeInSecs()
789+
- TEST_TOKEN_EXPIRATION_TIMESTAMP
790+
+ TimeUnit.MINUTES.toSeconds(30),
784791
TEST_AUTH_TOKEN,
785792
TEST_TOKEN_EXPIRATION_TIMESTAMP));
786793

0 commit comments

Comments
 (0)