Skip to content

Commit a039e05

Browse files
authored
fix: Enabled automatic HTTP retries for FirebaseProjectManagement (#356)
* Enabled automatic HTTP retries for FirebaseProjectManagement * Added some test cases * Added helper function for disabling exponential backoff during tests * Added util class for simplifying retry tests * Simplified test cases * Removed unused method
1 parent 6457976 commit a039e05

File tree

5 files changed

+191
-19
lines changed

5 files changed

+191
-19
lines changed

src/main/java/com/google/firebase/internal/ApiClientUtils.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*/
3030
public class ApiClientUtils {
3131

32-
private static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.builder()
32+
static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.builder()
3333
.setMaxRetries(4)
3434
.setRetryStatusCodes(ImmutableList.of(500, 503))
3535
.setMaxIntervalMillis(60 * 1000)
@@ -43,9 +43,21 @@ public class ApiClientUtils {
4343
* @return A new {@code HttpRequestFactory} instance.
4444
*/
4545
public static HttpRequestFactory newAuthorizedRequestFactory(FirebaseApp app) {
46+
return newAuthorizedRequestFactory(app, DEFAULT_RETRY_CONFIG);
47+
}
48+
49+
/**
50+
* Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and
51+
* automatic retries.
52+
*
53+
* @param app {@link FirebaseApp} from which to obtain authorization credentials.
54+
* @param retryConfig {@link RetryConfig} instance or null to disable retries.
55+
* @return A new {@code HttpRequestFactory} instance.
56+
*/
57+
public static HttpRequestFactory newAuthorizedRequestFactory(
58+
FirebaseApp app, @Nullable RetryConfig retryConfig) {
4659
HttpTransport transport = app.getOptions().getHttpTransport();
47-
return transport.createRequestFactory(
48-
new FirebaseRequestInitializer(app, DEFAULT_RETRY_CONFIG));
60+
return transport.createRequestFactory(new FirebaseRequestInitializer(app, retryConfig));
4961
}
5062

5163
public static HttpRequestFactory newUnauthorizedRequestFactory(FirebaseApp app) {

src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static com.google.common.base.Preconditions.checkArgument;
1919
import static com.google.common.base.Preconditions.checkNotNull;
2020

21+
import com.google.api.client.http.HttpRequestFactory;
2122
import com.google.api.client.http.HttpResponseInterceptor;
2223
import com.google.api.client.util.Base64;
2324
import com.google.api.client.util.Key;
@@ -32,8 +33,8 @@
3233
import com.google.common.collect.ImmutableMap;
3334
import com.google.firebase.FirebaseApp;
3435
import com.google.firebase.ImplFirebaseTrampolines;
36+
import com.google.firebase.internal.ApiClientUtils;
3537
import com.google.firebase.internal.CallableOperation;
36-
import com.google.firebase.internal.FirebaseRequestInitializer;
3738
import java.nio.charset.StandardCharsets;
3839
import java.util.ArrayList;
3940
import java.util.List;
@@ -55,6 +56,7 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS
5556
private final FirebaseApp app;
5657
private final Sleeper sleeper;
5758
private final Scheduler scheduler;
59+
private final HttpRequestFactory requestFactory;
5860
private final HttpHelper httpHelper;
5961

6062
private final CreateAndroidAppFromAppIdFunction createAndroidAppFromAppIdFunction =
@@ -63,17 +65,26 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS
6365
new CreateIosAppFromAppIdFunction();
6466

6567
FirebaseProjectManagementServiceImpl(FirebaseApp app) {
66-
this(app, Sleeper.DEFAULT, new FirebaseAppScheduler(app));
68+
this(
69+
app,
70+
Sleeper.DEFAULT,
71+
new FirebaseAppScheduler(app),
72+
ApiClientUtils.newAuthorizedRequestFactory(app));
6773
}
6874

69-
FirebaseProjectManagementServiceImpl(FirebaseApp app, Sleeper sleeper, Scheduler scheduler) {
75+
@VisibleForTesting
76+
FirebaseProjectManagementServiceImpl(
77+
FirebaseApp app, Sleeper sleeper, Scheduler scheduler, HttpRequestFactory requestFactory) {
7078
this.app = checkNotNull(app);
7179
this.sleeper = checkNotNull(sleeper);
7280
this.scheduler = checkNotNull(scheduler);
73-
this.httpHelper = new HttpHelper(
74-
app.getOptions().getJsonFactory(),
75-
app.getOptions().getHttpTransport().createRequestFactory(
76-
new FirebaseRequestInitializer(app)));
81+
this.requestFactory = checkNotNull(requestFactory);
82+
this.httpHelper = new HttpHelper(app.getOptions().getJsonFactory(), requestFactory);
83+
}
84+
85+
@VisibleForTesting
86+
HttpRequestFactory getRequestFactory() {
87+
return requestFactory;
7788
}
7889

7990
@VisibleForTesting

src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ public void testAuthorizedHttpClient() throws IOException {
6969
assertEquals(retryConfig.getRetryStatusCodes(), ImmutableList.of(500, 503));
7070
}
7171

72+
@Test
73+
public void testAuthorizedHttpClientWithoutRetry() throws IOException {
74+
FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS);
75+
76+
HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app, null);
77+
78+
assertTrue(requestFactory.getInitializer() instanceof FirebaseRequestInitializer);
79+
HttpRequest request = requestFactory.buildGetRequest(TEST_URL);
80+
assertEquals("Bearer test-token", request.getHeaders().getAuthorization());
81+
HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler();
82+
assertFalse(retryHandler instanceof RetryHandlerDecorator);
83+
}
84+
7285
@Test
7386
public void testUnauthorizedHttpClient() throws IOException {
7487
FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS);
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2020 Google Inc.
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+
package com.google.firebase.internal;
18+
19+
import static com.google.firebase.internal.ApiClientUtils.DEFAULT_RETRY_CONFIG;
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertFalse;
22+
import static org.junit.Assert.assertTrue;
23+
24+
import com.google.api.client.http.GenericUrl;
25+
import com.google.api.client.http.HttpRequest;
26+
import com.google.api.client.http.HttpRequestFactory;
27+
import com.google.api.client.http.HttpUnsuccessfulResponseHandler;
28+
import com.google.api.client.testing.util.MockSleeper;
29+
import com.google.firebase.FirebaseApp;
30+
import com.google.firebase.internal.RetryInitializer.RetryHandlerDecorator;
31+
import java.io.IOException;
32+
33+
public class TestApiClientUtils {
34+
35+
private static final RetryConfig TEST_RETRY_CONFIG = RetryConfig.builder()
36+
.setMaxRetries(DEFAULT_RETRY_CONFIG.getMaxRetries())
37+
.setRetryStatusCodes(DEFAULT_RETRY_CONFIG.getRetryStatusCodes())
38+
.setMaxIntervalMillis(DEFAULT_RETRY_CONFIG.getMaxIntervalMillis())
39+
.setSleeper(new MockSleeper())
40+
.build();
41+
42+
private static final GenericUrl TEST_URL = new GenericUrl("https://firebase.google.com");
43+
44+
/**
45+
* Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and
46+
* automatic retries. Bypasses exponential backoff between consecutive retries for faster
47+
* execution during tests.
48+
*
49+
* @param app {@link FirebaseApp} from which to obtain authorization credentials.
50+
* @return A new {@code HttpRequestFactory} instance.
51+
*/
52+
public static HttpRequestFactory delayBypassedRequestFactory(FirebaseApp app) {
53+
return ApiClientUtils.newAuthorizedRequestFactory(app, TEST_RETRY_CONFIG);
54+
}
55+
56+
/**
57+
* Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts but
58+
* no retries.
59+
*
60+
* @param app {@link FirebaseApp} from which to obtain authorization credentials.
61+
* @return A new {@code HttpRequestFactory} instance.
62+
*/
63+
public static HttpRequestFactory retryDisabledRequestFactory(FirebaseApp app) {
64+
return ApiClientUtils.newAuthorizedRequestFactory(app, null);
65+
}
66+
67+
/**
68+
* Checks whther the given HttpRequestFactory has been configured for authorization and
69+
* automatic retries.
70+
*
71+
* @param requestFactory The HttpRequestFactory to check.
72+
*/
73+
public static void assertAuthAndRetrySupport(HttpRequestFactory requestFactory) {
74+
assertTrue(requestFactory.getInitializer() instanceof FirebaseRequestInitializer);
75+
HttpRequest request;
76+
try {
77+
request = requestFactory.buildGetRequest(TEST_URL);
78+
} catch (IOException e) {
79+
throw new RuntimeException("Failed to initialize request", e);
80+
}
81+
82+
// Verify authorization
83+
assertTrue(request.getHeaders().getAuthorization().startsWith("Bearer "));
84+
85+
// Verify retry support
86+
HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler();
87+
assertTrue(retryHandler instanceof RetryHandlerDecorator);
88+
RetryConfig retryConfig = ((RetryHandlerDecorator) retryHandler).getRetryHandler()
89+
.getRetryConfig();
90+
assertEquals(DEFAULT_RETRY_CONFIG.getMaxRetries(), retryConfig.getMaxRetries());
91+
assertEquals(DEFAULT_RETRY_CONFIG.getMaxIntervalMillis(), retryConfig.getMaxIntervalMillis());
92+
assertFalse(retryConfig.isRetryOnIOExceptions());
93+
assertEquals(DEFAULT_RETRY_CONFIG.getRetryStatusCodes(), retryConfig.getRetryStatusCodes());
94+
}
95+
}

src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import com.google.api.client.googleapis.util.Utils;
3131
import com.google.api.client.http.HttpRequest;
32+
import com.google.api.client.http.HttpRequestFactory;
3233
import com.google.api.client.http.HttpResponse;
3334
import com.google.api.client.http.HttpResponseInterceptor;
3435
import com.google.api.client.json.JsonParser;
@@ -43,6 +44,7 @@
4344
import com.google.firebase.FirebaseOptions;
4445
import com.google.firebase.TestOnlyImplFirebaseTrampolines;
4546
import com.google.firebase.auth.MockGoogleCredentials;
47+
import com.google.firebase.internal.TestApiClientUtils;
4648
import com.google.firebase.testing.MultiRequestMockHttpTransport;
4749
import java.io.ByteArrayOutputStream;
4850
import java.io.IOException;
@@ -370,7 +372,7 @@ public void listIosAppsAsyncMultiplePages() throws Exception {
370372
MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse();
371373
secondRpcResponse.setContent(LIST_IOS_APPS_PAGE_2_RESPONSE);
372374
serviceImpl = initServiceImpl(
373-
ImmutableList.<MockLowLevelHttpResponse>of(firstRpcResponse, secondRpcResponse),
375+
ImmutableList.of(firstRpcResponse, secondRpcResponse),
374376
interceptor);
375377

376378
List<IosApp> iosAppList = serviceImpl.listIosAppsAsync(PROJECT_ID).get();
@@ -400,7 +402,7 @@ public void createIosApp() throws Exception {
400402
MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse();
401403
thirdRpcResponse.setContent(CREATE_IOS_GET_OPERATION_ATTEMPT_2_RESPONSE);
402404
serviceImpl = initServiceImpl(
403-
ImmutableList.<MockLowLevelHttpResponse>of(
405+
ImmutableList.of(
404406
firstRpcResponse, secondRpcResponse, thirdRpcResponse),
405407
interceptor);
406408

@@ -624,7 +626,7 @@ public void listAndroidAppsMultiplePages() throws Exception {
624626
MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse();
625627
secondRpcResponse.setContent(LIST_ANDROID_APPS_PAGE_2_RESPONSE);
626628
serviceImpl = initServiceImpl(
627-
ImmutableList.<MockLowLevelHttpResponse>of(firstRpcResponse, secondRpcResponse),
629+
ImmutableList.of(firstRpcResponse, secondRpcResponse),
628630
interceptor);
629631

630632
List<AndroidApp> androidAppList = serviceImpl.listAndroidApps(PROJECT_ID);
@@ -652,7 +654,7 @@ public void listAndroidAppsAsyncMultiplePages() throws Exception {
652654
MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse();
653655
secondRpcResponse.setContent(LIST_ANDROID_APPS_PAGE_2_RESPONSE);
654656
serviceImpl = initServiceImpl(
655-
ImmutableList.<MockLowLevelHttpResponse>of(firstRpcResponse, secondRpcResponse),
657+
ImmutableList.of(firstRpcResponse, secondRpcResponse),
656658
interceptor);
657659

658660
List<AndroidApp> androidAppList = serviceImpl.listAndroidAppsAsync(PROJECT_ID).get();
@@ -682,7 +684,7 @@ public void createAndroidApp() throws Exception {
682684
MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse();
683685
thirdRpcResponse.setContent(CREATE_ANDROID_GET_OPERATION_ATTEMPT_2_RESPONSE);
684686
serviceImpl = initServiceImpl(
685-
ImmutableList.<MockLowLevelHttpResponse>of(
687+
ImmutableList.of(
686688
firstRpcResponse, secondRpcResponse, thirdRpcResponse),
687689
interceptor);
688690

@@ -714,7 +716,7 @@ public void createAndroidAppAsync() throws Exception {
714716
MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse();
715717
thirdRpcResponse.setContent(CREATE_ANDROID_GET_OPERATION_ATTEMPT_2_RESPONSE);
716718
serviceImpl = initServiceImpl(
717-
ImmutableList.<MockLowLevelHttpResponse>of(
719+
ImmutableList.of(
718720
firstRpcResponse, secondRpcResponse, thirdRpcResponse),
719721
interceptor);
720722

@@ -915,10 +917,48 @@ public void deleteShaCertificateAsync() throws Exception {
915917
checkRequestHeader(expectedUrl, HttpMethod.DELETE);
916918
}
917919

920+
@Test
921+
public void testAuthAndRetriesSupport() {
922+
FirebaseOptions options = new FirebaseOptions.Builder()
923+
.setCredentials(new MockGoogleCredentials("test-token"))
924+
.setProjectId(PROJECT_ID)
925+
.build();
926+
FirebaseApp app = FirebaseApp.initializeApp(options);
927+
928+
FirebaseProjectManagementServiceImpl serviceImpl =
929+
new FirebaseProjectManagementServiceImpl(app);
930+
931+
TestApiClientUtils.assertAuthAndRetrySupport(serviceImpl.getRequestFactory());
932+
}
933+
934+
@Test
935+
public void testHttpRetries() throws Exception {
936+
List<MockLowLevelHttpResponse> mockResponses = ImmutableList.of(
937+
firstRpcResponse.setStatusCode(503).setContent("{}"),
938+
new MockLowLevelHttpResponse().setContent("{}"));
939+
MockHttpTransport transport = new MultiRequestMockHttpTransport(mockResponses);
940+
FirebaseOptions options = new FirebaseOptions.Builder()
941+
.setCredentials(new MockGoogleCredentials("test-token"))
942+
.setProjectId(PROJECT_ID)
943+
.setHttpTransport(transport)
944+
.build();
945+
FirebaseApp app = FirebaseApp.initializeApp(options);
946+
HttpRequestFactory requestFactory = TestApiClientUtils.delayBypassedRequestFactory(app);
947+
FirebaseProjectManagementServiceImpl serviceImpl = new FirebaseProjectManagementServiceImpl(
948+
app, new MockSleeper(), new MockScheduler(), requestFactory);
949+
serviceImpl.setInterceptor(interceptor);
950+
951+
serviceImpl.deleteShaCertificate(SHA1_RESOURCE_NAME);
952+
953+
String expectedUrl = String.format(
954+
"%s/v1beta1/%s", FIREBASE_PROJECT_MANAGEMENT_URL, SHA1_RESOURCE_NAME);
955+
checkRequestHeader(expectedUrl, HttpMethod.DELETE);
956+
}
957+
918958
private static FirebaseProjectManagementServiceImpl initServiceImpl(
919959
MockLowLevelHttpResponse mockResponse,
920960
MultiRequestTestResponseInterceptor interceptor) {
921-
return initServiceImpl(ImmutableList.<MockLowLevelHttpResponse>of(mockResponse), interceptor);
961+
return initServiceImpl(ImmutableList.of(mockResponse), interceptor);
922962
}
923963

924964
private static FirebaseProjectManagementServiceImpl initServiceImpl(
@@ -931,8 +971,9 @@ private static FirebaseProjectManagementServiceImpl initServiceImpl(
931971
.setHttpTransport(transport)
932972
.build();
933973
FirebaseApp app = FirebaseApp.initializeApp(options);
934-
FirebaseProjectManagementServiceImpl serviceImpl =
935-
new FirebaseProjectManagementServiceImpl(app, new MockSleeper(), new MockScheduler());
974+
HttpRequestFactory requestFactory = TestApiClientUtils.retryDisabledRequestFactory(app);
975+
FirebaseProjectManagementServiceImpl serviceImpl = new FirebaseProjectManagementServiceImpl(
976+
app, new MockSleeper(), new MockScheduler(), requestFactory);
936977
serviceImpl.setInterceptor(interceptor);
937978
return serviceImpl;
938979
}

0 commit comments

Comments
 (0)