Skip to content

Commit fd6dec4

Browse files
qdpham13danasilver
authored andcommitted
Realtime RC - Check HTTP status before listening and retrying (#4121)
* Update client to check Http status codes before listening or connecting * Fix tests and add more tests. * Format files. * Close error stream * Update status code check and initialize response code to 0 * Change responseCode var to Integer and initialize to null
1 parent b772bdd commit fd6dec4

File tree

3 files changed

+116
-33
lines changed

3 files changed

+116
-33
lines changed

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,16 +97,9 @@ private String parseAndValidateConfigUpdateMessage(String message) {
9797
public void listenForNotifications() {
9898
if (httpURLConnection != null) {
9999
try {
100-
int responseCode = httpURLConnection.getResponseCode();
101-
if (responseCode == 200) {
102-
InputStream inputStream = httpURLConnection.getInputStream();
103-
handleNotifications(inputStream);
104-
inputStream.close();
105-
} else {
106-
propagateErrors(
107-
new FirebaseRemoteConfigRealtimeUpdateStreamException(
108-
"Http connection responded with error: " + responseCode));
109-
}
100+
InputStream inputStream = httpURLConnection.getInputStream();
101+
handleNotifications(inputStream);
102+
inputStream.close();
110103
} catch (IOException ex) {
111104
propagateErrors(
112105
new FirebaseRemoteConfigRealtimeUpdateFetchException(

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.TAG;
1818
import static com.google.firebase.remoteconfig.RemoteConfigConstants.REALTIME_REGEX_URL;
19+
import static com.google.firebase.remoteconfig.internal.ConfigFetchHandler.HTTP_TOO_MANY_REQUESTS;
1920

2021
import android.annotation.SuppressLint;
2122
import android.content.Context;
@@ -236,7 +237,9 @@ private URL getUrl() {
236237
return realtimeURL;
237238
}
238239

239-
private HttpURLConnection createRealtimeConnection() throws IOException {
240+
/** Create HTTP connection and set headers. */
241+
@SuppressLint("VisibleForTests")
242+
public HttpURLConnection createRealtimeConnection() throws IOException {
240243
URL realtimeUrl = getUrl();
241244
HttpURLConnection httpURLConnection = (HttpURLConnection) realtimeUrl.openConnection();
242245
setCommonRequestHeaders(httpURLConnection);
@@ -245,8 +248,9 @@ private HttpURLConnection createRealtimeConnection() throws IOException {
245248
return httpURLConnection;
246249
}
247250

248-
// Try to reopen HTTP connection after a random amount of time
249-
private synchronized void retryHTTPConnection() {
251+
/** Retries HTTP stream connection asyncly in random time intervals. */
252+
@SuppressLint("VisibleForTests")
253+
public synchronized void retryHTTPConnection() {
250254
if (canMakeHttpStreamConnection() && httpRetriesRemaining > 0) {
251255
if (httpRetriesRemaining < ORIGINAL_RETRIES) {
252256
httpRetrySeconds *= getRetryMultiplier();
@@ -273,7 +277,12 @@ synchronized void stopRealtime() {
273277
scheduledExecutorService.shutdownNow();
274278
}
275279

276-
private synchronized ConfigAutoFetch startAutoFetch(HttpURLConnection httpURLConnection) {
280+
/**
281+
* Create Autofetch class that listens on HTTP stream for ConfigUpdate messages and calls Fetch
282+
* accordingly.
283+
*/
284+
@SuppressLint("VisibleForTests")
285+
public synchronized ConfigAutoFetch startAutoFetch(HttpURLConnection httpURLConnection) {
277286
ConfigUpdateListener retryCallback =
278287
new ConfigUpdateListener() {
279288
@Override
@@ -298,6 +307,15 @@ public void onError(Exception error) {
298307
return new ConfigAutoFetch(httpURLConnection, configFetchHandler, listeners, retryCallback);
299308
}
300309

310+
// HTTP status code that the Realtime client should retry on.
311+
private boolean isStatusCodeRetryable(int statusCode) {
312+
return statusCode == HttpURLConnection.HTTP_CLIENT_TIMEOUT
313+
|| statusCode == HTTP_TOO_MANY_REQUESTS
314+
|| statusCode == HttpURLConnection.HTTP_BAD_GATEWAY
315+
|| statusCode == HttpURLConnection.HTTP_UNAVAILABLE
316+
|| statusCode == HttpURLConnection.HTTP_GATEWAY_TIMEOUT;
317+
}
318+
301319
/**
302320
* Open the realtime connection, begin listening for updates, and auto-fetch when an update is
303321
* received.
@@ -306,39 +324,55 @@ public void onError(Exception error) {
306324
* chunk-encoded HTTP body. When the connection closes, it attempts to reestablish the stream.
307325
*/
308326
@SuppressLint("VisibleForTests")
309-
synchronized void beginRealtimeHttpStream() {
327+
public synchronized void beginRealtimeHttpStream() {
310328
if (!canMakeHttpStreamConnection()) {
311329
return;
312330
}
313331

332+
Integer responseCode = null;
314333
try {
315334
// Create the open the connection.
316335
httpURLConnection = createRealtimeConnection();
317-
httpURLConnection.connect();
336+
responseCode = httpURLConnection.getResponseCode();
318337

319-
// Reset the retries remaining if we opened the connection without an exception.
320-
resetRetryParameters();
338+
// If the connection returned a 200 response code, start listening for messages.
339+
if (responseCode == HttpURLConnection.HTTP_OK) {
340+
// Reset the retries remaining if we opened the connection without an exception.
341+
resetRetryParameters();
321342

322-
// Start listening for realtime notifications.
323-
ConfigAutoFetch configAutoFetch = startAutoFetch(httpURLConnection);
324-
configAutoFetch.listenForNotifications();
343+
// Start listening for realtime notifications.
344+
ConfigAutoFetch configAutoFetch = startAutoFetch(httpURLConnection);
345+
configAutoFetch.listenForNotifications();
346+
}
325347
} catch (IOException e) {
326348
Log.d(TAG, "Exception connecting to realtime stream. Retrying the connection...");
327349
} finally {
328350
closeRealtimeHttpStream();
329-
retryHTTPConnection();
351+
352+
// If responseCode is null then no connection was made to server and the SDK should still
353+
// retry.
354+
if (responseCode == null
355+
|| responseCode == HttpURLConnection.HTTP_OK
356+
|| isStatusCodeRetryable(responseCode)) {
357+
retryHTTPConnection();
358+
} else {
359+
propagateErrors(
360+
new FirebaseRemoteConfigRealtimeUpdateStreamException(
361+
"The server returned a status code that is not retryable. Realtime is shutting down."));
362+
}
330363
}
331364
}
332365

333366
// Pauses Http stream listening
334-
synchronized void closeRealtimeHttpStream() {
367+
public synchronized void closeRealtimeHttpStream() {
335368
if (httpURLConnection != null) {
336369
this.httpURLConnection.disconnect();
337370

338371
// Explicitly close the input stream due to a bug in the Android okhttp implementation.
339372
// See github.com/firebase/firebase-android-sdk/pull/808.
340373
try {
341374
this.httpURLConnection.getInputStream().close();
375+
this.httpURLConnection.getErrorStream().close();
342376
} catch (IOException e) {
343377
}
344378
this.httpURLConnection = null;

firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@
3030
import static org.mockito.ArgumentMatchers.eq;
3131
import static org.mockito.Matchers.any;
3232
import static org.mockito.Mockito.doAnswer;
33+
import static org.mockito.Mockito.doNothing;
34+
import static org.mockito.Mockito.doReturn;
3335
import static org.mockito.Mockito.doThrow;
3436
import static org.mockito.Mockito.never;
37+
import static org.mockito.Mockito.spy;
3538
import static org.mockito.Mockito.times;
3639
import static org.mockito.Mockito.verify;
3740
import static org.mockito.Mockito.when;
@@ -65,6 +68,7 @@
6568
import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler;
6669
import com.google.firebase.remoteconfig.internal.ConfigMetadataClient;
6770
import com.google.firebase.remoteconfig.internal.ConfigRealtimeHandler;
71+
import com.google.firebase.remoteconfig.internal.ConfigRealtimeHttpClient;
6872
import com.google.firebase.remoteconfig.internal.Personalization;
6973
import java.io.ByteArrayInputStream;
7074
import java.io.IOException;
@@ -142,6 +146,7 @@ public final class FirebaseRemoteConfigTest {
142146
@Mock private ConfigMetadataClient metadataClient;
143147

144148
@Mock private ConfigRealtimeHandler mockConfigRealtimeHandler;
149+
@Mock private ConfigAutoFetch mockConfigAutoFetch;
145150
@Mock private ConfigUpdateListenerRegistration mockRealtimeRegistration;
146151
@Mock private HttpURLConnection mockHttpURLConnection;
147152
@Mock private ConfigUpdateListener mockListener;
@@ -167,6 +172,7 @@ public final class FirebaseRemoteConfigTest {
167172
private FetchResponse realtimeFetchedContainerResponse;
168173
private ConfigContainer realtimeFetchedContainer;
169174
private ConfigAutoFetch configAutoFetch;
175+
private ConfigRealtimeHttpClient configRealtimeHttpClient;
170176

171177
private FetchResponse firstFetchedContainerResponse;
172178

@@ -271,6 +277,14 @@ public void setUp() throws Exception {
271277
listeners.add(mockListener);
272278
configAutoFetch =
273279
new ConfigAutoFetch(mockHttpURLConnection, mockFetchHandler, listeners, mockRetryListener);
280+
configRealtimeHttpClient =
281+
new ConfigRealtimeHttpClient(
282+
firebaseApp,
283+
mockFirebaseInstallations,
284+
mockFetchHandler,
285+
context,
286+
"firebase",
287+
listeners);
274288
}
275289

276290
@Test
@@ -1109,7 +1123,6 @@ public void realtime_client_removeListener_success() {
11091123

11101124
@Test
11111125
public void realtime_stream_listen_and_retry_success() throws Exception {
1112-
when(mockHttpURLConnection.getResponseCode()).thenReturn(200);
11131126
when(mockHttpURLConnection.getInputStream())
11141127
.thenReturn(
11151128
new ByteArrayInputStream(
@@ -1123,17 +1136,60 @@ public void realtime_stream_listen_and_retry_success() throws Exception {
11231136

11241137
@Test
11251138
public void realtime_stream_listen_fail() throws Exception {
1126-
when(mockHttpURLConnection.getResponseCode()).thenReturn(400);
1127-
when(mockHttpURLConnection.getInputStream())
1128-
.thenReturn(
1129-
new ByteArrayInputStream(
1130-
"{\\r\\n \\\"latestTemplateVersionNumber\\\": 1\\r\\n}"
1131-
.getBytes(StandardCharsets.UTF_8)));
1132-
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
1133-
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
1139+
when(mockHttpURLConnection.getInputStream()).thenThrow(IOException.class);
11341140
configAutoFetch.listenForNotifications();
11351141

1136-
verify(mockListener).onError(any(FirebaseRemoteConfigRealtimeUpdateStreamException.class));
1142+
verify(mockListener).onError(any(FirebaseRemoteConfigRealtimeUpdateFetchException.class));
1143+
}
1144+
1145+
@Test
1146+
public void realtime_redirectStatusCode_noRetries() throws Exception {
1147+
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
1148+
doReturn(mockHttpURLConnection).when(configRealtimeHttpClientSpy).createRealtimeConnection();
1149+
doNothing().when(configRealtimeHttpClientSpy).closeRealtimeHttpStream();
1150+
when(mockHttpURLConnection.getResponseCode()).thenReturn(301);
1151+
configRealtimeHttpClientSpy.beginRealtimeHttpStream();
1152+
verify(configRealtimeHttpClientSpy, never()).startAutoFetch(any());
1153+
verify(configRealtimeHttpClientSpy, never()).retryHTTPConnection();
1154+
}
1155+
1156+
@Test
1157+
public void realtime_okStatusCode_startAutofetchAndRetries() throws Exception {
1158+
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
1159+
doReturn(mockHttpURLConnection).when(configRealtimeHttpClientSpy).createRealtimeConnection();
1160+
doReturn(mockConfigAutoFetch).when(configRealtimeHttpClientSpy).startAutoFetch(any());
1161+
doNothing().when(configRealtimeHttpClientSpy).retryHTTPConnection();
1162+
doNothing().when(configRealtimeHttpClientSpy).closeRealtimeHttpStream();
1163+
when(mockHttpURLConnection.getResponseCode()).thenReturn(200);
1164+
1165+
configRealtimeHttpClientSpy.beginRealtimeHttpStream();
1166+
verify(mockConfigAutoFetch).listenForNotifications();
1167+
verify(configRealtimeHttpClientSpy).retryHTTPConnection();
1168+
}
1169+
1170+
@Test
1171+
public void realtime_badGatewayStatusCode_noAutofetchButRetries() throws Exception {
1172+
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
1173+
doReturn(mockHttpURLConnection).when(configRealtimeHttpClientSpy).createRealtimeConnection();
1174+
doNothing().when(configRealtimeHttpClientSpy).retryHTTPConnection();
1175+
doNothing().when(configRealtimeHttpClientSpy).closeRealtimeHttpStream();
1176+
when(mockHttpURLConnection.getResponseCode()).thenReturn(502);
1177+
1178+
configRealtimeHttpClientSpy.beginRealtimeHttpStream();
1179+
verify(configRealtimeHttpClientSpy, never()).startAutoFetch(any());
1180+
verify(configRealtimeHttpClientSpy).retryHTTPConnection();
1181+
}
1182+
1183+
@Test
1184+
public void realtime_exceptionThrown_noAutofetchButRetries() throws Exception {
1185+
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
1186+
doThrow(IOException.class).when(configRealtimeHttpClientSpy).createRealtimeConnection();
1187+
doNothing().when(configRealtimeHttpClientSpy).retryHTTPConnection();
1188+
doNothing().when(configRealtimeHttpClientSpy).closeRealtimeHttpStream();
1189+
1190+
configRealtimeHttpClientSpy.beginRealtimeHttpStream();
1191+
verify(configRealtimeHttpClientSpy, never()).startAutoFetch(any());
1192+
verify(configRealtimeHttpClientSpy).retryHTTPConnection();
11371193
}
11381194

11391195
@Test

0 commit comments

Comments
 (0)