Skip to content

Commit

Permalink
Refactor real-time RC to use tasks for auth token requests (#5093)
Browse files Browse the repository at this point in the history
* Refactor real-time RC to use tasks for auth token requests

* Add comments and update log messaging

* Add comments

* Update comments and address PR comments
  • Loading branch information
qdpham13 authored Jun 26, 2023
1 parent b7aae80 commit ba08f8f
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -152,17 +152,6 @@ private static String extractProjectNumberFromAppId(String gmpAppId) {
return matcher.matches() ? matcher.group(1) : null;
}

private void getInstallationAuthToken(HttpURLConnection httpURLConnection) {
Task<InstallationTokenResult> installationAuthTokenTask = firebaseInstallations.getToken(false);
installationAuthTokenTask.onSuccessTask(
scheduledExecutorService,
unusedToken -> {
httpURLConnection.setRequestProperty(
INSTALLATIONS_AUTH_TOKEN_HEADER, unusedToken.getToken());
return Tasks.forResult(null);
});
}

/** Gets the Android package's SHA-1 fingerprint. */
private String getFingerprintHashForPackage() {
byte[] hash;
Expand All @@ -183,9 +172,9 @@ private String getFingerprintHashForPackage() {
}
}

private void setCommonRequestHeaders(HttpURLConnection httpURLConnection) {
// Get Installation Token
getInstallationAuthToken(httpURLConnection);
private void setCommonRequestHeaders(HttpURLConnection httpURLConnection, String authToken) {
// Auth token
httpURLConnection.setRequestProperty(INSTALLATIONS_AUTH_TOKEN_HEADER, authToken);

// API Key
httpURLConnection.setRequestProperty(API_KEY_HEADER, this.firebaseApp.getOptions().getApiKey());
Expand Down Expand Up @@ -315,13 +304,37 @@ private URL getUrl() {

/** Create HTTP connection and set headers. */
@SuppressLint("VisibleForTests")
public HttpURLConnection createRealtimeConnection() throws IOException {
URL realtimeUrl = getUrl();
HttpURLConnection httpURLConnection = (HttpURLConnection) realtimeUrl.openConnection();
setCommonRequestHeaders(httpURLConnection);
setRequestParams(httpURLConnection);

return httpURLConnection;
public Task<HttpURLConnection> createRealtimeConnection() {
// Make async call to get auth token.
Task<InstallationTokenResult> installationAuthTokenTask = firebaseInstallations.getToken(false);
// When the auth token task has finished, set up HTTP connection with headers and params.
return Tasks.whenAllComplete(installationAuthTokenTask)
.continueWithTask(
this.scheduledExecutorService,
(completedInstallationTask) -> {
if (!installationAuthTokenTask.isSuccessful()) {
return Tasks.forException(
new FirebaseRemoteConfigClientException(
"Firebase Installations failed to get installation auth token for real-time.",
installationAuthTokenTask.getException()));
}

HttpURLConnection httpURLConnection;
try {
URL realtimeUrl = getUrl();
httpURLConnection = (HttpURLConnection) realtimeUrl.openConnection();

String installationAuthToken = installationAuthTokenTask.getResult().getToken();
setCommonRequestHeaders(httpURLConnection, installationAuthToken);
setRequestParams(httpURLConnection);
} catch (IOException ex) {
return Tasks.forException(
new FirebaseRemoteConfigClientException(
"Failed to open HTTP stream connection", ex));
}

return Tasks.forResult(httpURLConnection);
});
}

/** Initial Http stream attempt that makes call without waiting. */
Expand Down Expand Up @@ -455,59 +468,83 @@ public void beginRealtimeHttpStream() {
return;
}

Integer responseCode = null;
HttpURLConnection httpURLConnection = null;
setIsHttpConnectionRunning(true);
try {
// Create the open the connection.
httpURLConnection = createRealtimeConnection();
responseCode = httpURLConnection.getResponseCode();

// If the connection returned a 200 response code, start listening for messages.
if (responseCode == HttpURLConnection.HTTP_OK) {
// Reset the retries remaining if we opened the connection without an exception.
resetRetryCount();
metadataClient.resetRealtimeBackoff();

// Start listening for realtime notifications.
ConfigAutoFetch configAutoFetch = startAutoFetch(httpURLConnection);
configAutoFetch.listenForNotifications();
}
} catch (IOException e) {
// Stream could not be open due to a transient issue and the system will retry the connection
// without user intervention.
Log.d(TAG, "Exception connecting to real-time RC backend. Retrying the connection...", e);
} finally {
closeRealtimeHttpStream(httpURLConnection);
setIsHttpConnectionRunning(false);

boolean connectionFailed = responseCode == null || isStatusCodeRetryable(responseCode);
if (connectionFailed) {
updateBackoffMetadataWithLastFailedStreamConnectionTime(
new Date(clock.currentTimeMillis()));
}

// If responseCode is null then no connection was made to server and the SDK should still
// retry.
if (connectionFailed || responseCode == HttpURLConnection.HTTP_OK) {
retryHttpConnectionWhenBackoffEnds();
} else {
String errorMessage =
String.format(
"Unable to connect to the server. Try again in a few minutes. HTTP status code: %d",
responseCode);
// Return server message for when the Firebase Remote Config Realtime API is disabled and
// the server returns a 403
if (responseCode == 403) {
errorMessage = parseForbiddenErrorResponseMessage(httpURLConnection.getErrorStream());
}
propagateErrors(
new FirebaseRemoteConfigServerException(
responseCode,
errorMessage,
FirebaseRemoteConfigException.Code.CONFIG_UPDATE_STREAM_ERROR));
}
}
// Make async call to create and setup HTTP connection.
Task<HttpURLConnection> httpURLConnectionTask = createRealtimeConnection();
// When the connection task has finished, begin real-time actions.
Tasks.whenAllComplete(httpURLConnectionTask)
.continueWith(
this.scheduledExecutorService,
(completedHttpUrlConnectionTask) -> {
Integer responseCode = null;
HttpURLConnection httpURLConnection = null;

try {
// If HTTP connection task failed throw exception to move to the catch block.
if (!httpURLConnectionTask.isSuccessful()) {
throw new IOException(httpURLConnectionTask.getException());
}
setIsHttpConnectionRunning(true);

// Get HTTP connection and check response code.
httpURLConnection = httpURLConnectionTask.getResult();
responseCode = httpURLConnection.getResponseCode();

// If the connection returned a 200 response code, start listening for messages.
if (responseCode == HttpURLConnection.HTTP_OK) {
// Reset the retries remaining if we opened the connection without an exception.
resetRetryCount();
metadataClient.resetRealtimeBackoff();

// Start listening for realtime notifications.
ConfigAutoFetch configAutoFetch = startAutoFetch(httpURLConnection);
configAutoFetch.listenForNotifications();
}
} catch (IOException e) {
// Stream could not be open due to a transient issue and the system will retry the
// connection
// without user intervention.
Log.d(
TAG,
"Exception connecting to real-time RC backend. Retrying the connection...",
e);
} finally {
closeRealtimeHttpStream(httpURLConnection);
setIsHttpConnectionRunning(false);

boolean connectionFailed =
responseCode == null || isStatusCodeRetryable(responseCode);
if (connectionFailed) {
updateBackoffMetadataWithLastFailedStreamConnectionTime(
new Date(clock.currentTimeMillis()));
}

// If responseCode is null then no connection was made to server and the SDK should
// still
// retry.
if (connectionFailed || responseCode == HttpURLConnection.HTTP_OK) {
retryHttpConnectionWhenBackoffEnds();
} else {
String errorMessage =
String.format(
"Unable to connect to the server. Try again in a few minutes. HTTP status code: %d",
responseCode);
// Return server message for when the Firebase Remote Config Realtime API is
// disabled and
// the server returns a 403
if (responseCode == 403) {
errorMessage =
parseForbiddenErrorResponseMessage(httpURLConnection.getErrorStream());
}
propagateErrors(
new FirebaseRemoteConfigServerException(
responseCode,
errorMessage,
FirebaseRemoteConfigException.Code.CONFIG_UPDATE_STREAM_ERROR));
}
}

return Tasks.forResult(null);
});
}

// Pauses Http stream listening
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1225,12 +1225,16 @@ public void realtime_fetchesWithoutChangedParams_doesNotCallOnUpdate() throws Ex
@Test
public void realtime_redirectStatusCode_noRetries() throws Exception {
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
doReturn(mockHttpURLConnection).when(configRealtimeHttpClientSpy).createRealtimeConnection();
doReturn(Tasks.forResult(mockHttpURLConnection))
.when(configRealtimeHttpClientSpy)
.createRealtimeConnection();
doNothing()
.when(configRealtimeHttpClientSpy)
.closeRealtimeHttpStream(any(HttpURLConnection.class));
when(mockHttpURLConnection.getResponseCode()).thenReturn(301);

configRealtimeHttpClientSpy.beginRealtimeHttpStream();
flushScheduledTasks();

verify(configRealtimeHttpClientSpy, never()).startAutoFetch(any());
verify(configRealtimeHttpClientSpy, never()).retryHttpConnectionWhenBackoffEnds();
Expand All @@ -1240,7 +1244,9 @@ public void realtime_redirectStatusCode_noRetries() throws Exception {
@Test
public void realtime_okStatusCode_startAutofetchAndRetries() throws Exception {
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
doReturn(mockHttpURLConnection).when(configRealtimeHttpClientSpy).createRealtimeConnection();
doReturn(Tasks.forResult(mockHttpURLConnection))
.when(configRealtimeHttpClientSpy)
.createRealtimeConnection();
doReturn(mockConfigAutoFetch).when(configRealtimeHttpClientSpy).startAutoFetch(any());
doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds();
doNothing()
Expand All @@ -1249,29 +1255,37 @@ public void realtime_okStatusCode_startAutofetchAndRetries() throws Exception {
when(mockHttpURLConnection.getResponseCode()).thenReturn(200);

configRealtimeHttpClientSpy.beginRealtimeHttpStream();
flushScheduledTasks();

verify(mockConfigAutoFetch).listenForNotifications();
verify(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds();
}

@Test
public void realtime_badGatewayStatusCode_noAutofetchButRetries() throws Exception {
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
doReturn(mockHttpURLConnection).when(configRealtimeHttpClientSpy).createRealtimeConnection();
doReturn(Tasks.forResult(mockHttpURLConnection))
.when(configRealtimeHttpClientSpy)
.createRealtimeConnection();
doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds();
doNothing()
.when(configRealtimeHttpClientSpy)
.closeRealtimeHttpStream(any(HttpURLConnection.class));
when(mockHttpURLConnection.getResponseCode()).thenReturn(502);

configRealtimeHttpClientSpy.beginRealtimeHttpStream();
flushScheduledTasks();

verify(configRealtimeHttpClientSpy, never()).startAutoFetch(any());
verify(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds();
}

@Test
public void realtime_retryableStatusCode_increasesConfigMetadataFailedStreams() throws Exception {
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
doReturn(mockHttpURLConnection).when(configRealtimeHttpClientSpy).createRealtimeConnection();
doReturn(Tasks.forResult(mockHttpURLConnection))
.when(configRealtimeHttpClientSpy)
.createRealtimeConnection();
doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds();
doNothing()
.when(configRealtimeHttpClientSpy)
Expand All @@ -1280,13 +1294,17 @@ public void realtime_retryableStatusCode_increasesConfigMetadataFailedStreams()
int failedStreams = configRealtimeHttpClientSpy.getNumberOfFailedStreams();

configRealtimeHttpClientSpy.beginRealtimeHttpStream();
flushScheduledTasks();

assertThat(configRealtimeHttpClientSpy.getNumberOfFailedStreams()).isEqualTo(failedStreams + 1);
}

@Test
public void realtime_retryableStatusCode_increasesConfigMetadataBackoffDate() throws Exception {
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
doReturn(mockHttpURLConnection).when(configRealtimeHttpClientSpy).createRealtimeConnection();
doReturn(Tasks.forResult(mockHttpURLConnection))
.when(configRealtimeHttpClientSpy)
.createRealtimeConnection();
doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds();
doNothing()
.when(configRealtimeHttpClientSpy)
Expand All @@ -1295,14 +1313,18 @@ public void realtime_retryableStatusCode_increasesConfigMetadataBackoffDate() th
Date backoffDate = configRealtimeHttpClientSpy.getBackoffEndTime();

configRealtimeHttpClientSpy.beginRealtimeHttpStream();
flushScheduledTasks();

assertTrue(configRealtimeHttpClientSpy.getBackoffEndTime().after(backoffDate));
}

@Test
public void realtime_successfulStatusCode_doesNotIncreaseConfigMetadataFailedStreams()
throws Exception {
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
doReturn(mockHttpURLConnection).when(configRealtimeHttpClientSpy).createRealtimeConnection();
doReturn(Tasks.forResult(mockHttpURLConnection))
.when(configRealtimeHttpClientSpy)
.createRealtimeConnection();
doReturn(mockConfigAutoFetch).when(configRealtimeHttpClientSpy).startAutoFetch(any());
doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds();
doNothing()
Expand All @@ -1312,14 +1334,18 @@ public void realtime_successfulStatusCode_doesNotIncreaseConfigMetadataFailedStr
int failedStreams = configRealtimeHttpClientSpy.getNumberOfFailedStreams();

configRealtimeHttpClientSpy.beginRealtimeHttpStream();
flushScheduledTasks();

assertThat(configRealtimeHttpClientSpy.getNumberOfFailedStreams()).isEqualTo(failedStreams);
}

@Test
public void realtime_successfulStatusCode_doesNotIncreaseConfigMetadataBackoffDate()
throws Exception {
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
doReturn(mockHttpURLConnection).when(configRealtimeHttpClientSpy).createRealtimeConnection();
doReturn(Tasks.forResult(mockHttpURLConnection))
.when(configRealtimeHttpClientSpy)
.createRealtimeConnection();
doReturn(mockConfigAutoFetch).when(configRealtimeHttpClientSpy).startAutoFetch(any());
doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds();
doNothing()
Expand All @@ -1329,21 +1355,27 @@ public void realtime_successfulStatusCode_doesNotIncreaseConfigMetadataBackoffDa
Date backoffDate = configRealtimeHttpClientSpy.getBackoffEndTime();

configRealtimeHttpClientSpy.beginRealtimeHttpStream();
flushScheduledTasks();

assertFalse(configRealtimeHttpClientSpy.getBackoffEndTime().after(backoffDate));
}

@Test
public void realtime_forbiddenStatusCode_returnsStreamError() throws Exception {
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
doReturn(mockHttpURLConnection).when(configRealtimeHttpClientSpy).createRealtimeConnection();
doReturn(Tasks.forResult(mockHttpURLConnection))
.when(configRealtimeHttpClientSpy)
.createRealtimeConnection();
doNothing()
.when(configRealtimeHttpClientSpy)
.closeRealtimeHttpStream(any(HttpURLConnection.class));
when(mockHttpURLConnection.getErrorStream())
.thenReturn(
new ByteArrayInputStream(FORBIDDEN_ERROR_MESSAGE.getBytes(StandardCharsets.UTF_8)));
when(mockHttpURLConnection.getResponseCode()).thenReturn(403);

configRealtimeHttpClientSpy.beginRealtimeHttpStream();
flushScheduledTasks();

verify(configRealtimeHttpClientSpy, never()).startAutoFetch(any());
verify(configRealtimeHttpClientSpy, never()).retryHttpConnectionWhenBackoffEnds();
Expand All @@ -1353,13 +1385,17 @@ public void realtime_forbiddenStatusCode_returnsStreamError() throws Exception {
@Test
public void realtime_exceptionThrown_noAutofetchButRetries() throws Exception {
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
doThrow(IOException.class).when(configRealtimeHttpClientSpy).createRealtimeConnection();
doReturn(Tasks.forException(new IOException()))
.when(configRealtimeHttpClientSpy)
.createRealtimeConnection();
doNothing().when(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds();
doNothing()
.when(configRealtimeHttpClientSpy)
.closeRealtimeHttpStream(any(HttpURLConnection.class));

configRealtimeHttpClientSpy.beginRealtimeHttpStream();
flushScheduledTasks();

verify(configRealtimeHttpClientSpy, never()).startAutoFetch(any());
verify(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds();
}
Expand Down

0 comments on commit ba08f8f

Please sign in to comment.