diff --git a/firebase-config/CHANGELOG.md b/firebase-config/CHANGELOG.md index fc00b486a87..743dd399729 100644 --- a/firebase-config/CHANGELOG.md +++ b/firebase-config/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased - +* [changed] This update introduces improvements to how the SDK handles real-time requests when a + Firebase project has exceeded its available quota for real-time services. Released in anticipation + of future quota enforcement, this change is designed to fetch the latest template even when the + quota is exhausted. # 22.1.2 * [fixed] Fixed `NetworkOnMainThreadException` on Android versions below 8 by disconnecting diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java index a93b1dc5784..ea13502b5d5 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java @@ -19,6 +19,8 @@ import android.util.Log; import androidx.annotation.GuardedBy; import androidx.annotation.VisibleForTesting; +import com.google.android.gms.common.util.Clock; +import com.google.android.gms.common.util.DefaultClock; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.remoteconfig.ConfigUpdate; @@ -31,6 +33,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; +import java.util.Date; import java.util.Random; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; @@ -43,6 +46,7 @@ public class ConfigAutoFetch { private static final int MAXIMUM_FETCH_ATTEMPTS = 3; private static final String TEMPLATE_VERSION_KEY = "latestTemplateVersionNumber"; private static final String REALTIME_DISABLED_KEY = "featureDisabled"; + private static final String REALTIME_RETRY_INTERVAL = "retryIntervalSeconds"; @GuardedBy("this") private final Set eventListeners; @@ -54,6 +58,7 @@ public class ConfigAutoFetch { private final ConfigUpdateListener retryCallback; private final ScheduledExecutorService scheduledExecutorService; private final Random random; + private final Clock clock; private boolean isInBackground; public ConfigAutoFetch( @@ -71,6 +76,18 @@ public ConfigAutoFetch( this.scheduledExecutorService = scheduledExecutorService; this.random = new Random(); this.isInBackground = false; + clock = DefaultClock.getInstance(); + } + + // Increase the backoff duration with a new end time based on Retry Interval + private synchronized void updateBackoffMetadataWithRetryInterval( + int realtimeRetryInterval, ConfigSharedPrefsClient sharedPrefsClient) { + Date currentTime = new Date(clock.currentTimeMillis()); + long backoffDurationInMillis = realtimeRetryInterval * 1000L; + Date backoffEndTime = new Date(currentTime.getTime() + backoffDurationInMillis); + + // Persist the new values to disk-backed metadata. + sharedPrefsClient.setRealtimeBackoffEndTime(backoffEndTime); } private synchronized void propagateErrors(FirebaseRemoteConfigException exception) { @@ -106,7 +123,7 @@ private String parseAndValidateConfigUpdateMessage(String message) { // Check connection and establish InputStream @VisibleForTesting - public void listenForNotifications() { + public void listenForNotifications(ConfigSharedPrefsClient sharedPrefsClient) { if (httpURLConnection == null) { return; } @@ -116,7 +133,7 @@ public void listenForNotifications() { InputStream inputStream = null; try { inputStream = httpURLConnection.getInputStream(); - handleNotifications(inputStream); + handleNotifications(inputStream, sharedPrefsClient); } catch (IOException ex) { // If the real-time connection is at an unexpected lifecycle state when the app is // backgrounded, it's expected closing the httpURLConnection will throw an exception. @@ -138,7 +155,8 @@ public void listenForNotifications() { } // Auto-fetch new config and execute callbacks on each new message - private void handleNotifications(InputStream inputStream) throws IOException { + private void handleNotifications( + InputStream inputStream, ConfigSharedPrefsClient sharedPrefsClient) throws IOException { BufferedReader reader = new BufferedReader((new InputStreamReader(inputStream, "utf-8"))); String partialConfigUpdateMessage; String currentConfigUpdateMessage = ""; @@ -190,6 +208,11 @@ private void handleNotifications(InputStream inputStream) throws IOException { autoFetch(MAXIMUM_FETCH_ATTEMPTS, targetTemplateVersion); } } + + if (jsonObject.has(REALTIME_RETRY_INTERVAL)) { + int realtimeRetryInterval = jsonObject.getInt(REALTIME_RETRY_INTERVAL); + updateBackoffMetadataWithRetryInterval(realtimeRetryInterval, sharedPrefsClient); + } } catch (JSONException ex) { // Message was mangled up and so it was unable to be parsed. User is notified of this // because it there could be a new configuration that needs to be fetched. diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java index 7be3ef97136..a700d5151ef 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java @@ -554,7 +554,7 @@ public void beginRealtimeHttpStream() { // Start listening for realtime notifications. configAutoFetch = startAutoFetch(httpURLConnection); - configAutoFetch.listenForNotifications(); + configAutoFetch.listenForNotifications(sharedPrefsClient); } } catch (IOException e) { if (isInBackground) { diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java index 7ce24bc44f6..b19431fa5a1 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java @@ -394,6 +394,16 @@ void setRealtimeBackoffMetadata(int numFailedStreams, Date backoffEndTime) { } } + @VisibleForTesting + public void setRealtimeBackoffEndTime(Date backoffEndTime) { + synchronized (realtimeBackoffMetadataLock) { + frcSharedPrefs + .edit() + .putLong(REALTIME_BACKOFF_END_TIME_IN_MILLIS_KEY, backoffEndTime.getTime()) + .apply(); + } + } + void resetRealtimeBackoff() { setRealtimeBackoffMetadata(NO_FAILED_REALTIME_STREAMS, NO_BACKOFF_TIME); } diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index 9e8f65c767e..76d36a20cd0 100644 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -1286,7 +1286,7 @@ public void realtime_stream_listen_and_end_connection() throws Exception { when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber( ConfigFetchHandler.FetchType.REALTIME, 1)) .thenReturn(Tasks.forResult(realtimeFetchedContainerResponse)); - configAutoFetch.listenForNotifications(); + configAutoFetch.listenForNotifications(sharedPrefsClient); verify(inputStreamSpy, times(2)).close(); } @@ -1299,7 +1299,7 @@ public void realtime_fetchesWithoutChangedParams_doesNotCallOnUpdate() throws Ex "{ \"latestTemplateVersionNumber\": 1 }".getBytes(StandardCharsets.UTF_8))); when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L); when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(firstFetchedContainerResponse)); - configAutoFetch.listenForNotifications(); + configAutoFetch.listenForNotifications(sharedPrefsClient); verifyNoInteractions(mockOnUpdateListener); } @@ -1339,7 +1339,7 @@ public void realtime_okStatusCode_startAutofetchAndRetries() throws Exception { configRealtimeHttpClientSpy.beginRealtimeHttpStream(); flushScheduledTasks(); - verify(mockConfigAutoFetch).listenForNotifications(); + verify(mockConfigAutoFetch).listenForNotifications(any()); verify(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds(); } @@ -1490,7 +1490,7 @@ public void realtime_stream_listen_and_failsafe_enabled() throws Exception { new ByteArrayInputStream( "{ \"featureDisabled\": true }".getBytes(StandardCharsets.UTF_8))); when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L); - configAutoFetch.listenForNotifications(); + configAutoFetch.listenForNotifications(sharedPrefsClient); verify(mockRetryListener).onError(any(FirebaseRemoteConfigServerException.class)); verify(mockFetchHandler, never()).fetch(0); @@ -1508,7 +1508,7 @@ public void realtime_stream_listen_and_failsafe_disabled() throws Exception { when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber( ConfigFetchHandler.FetchType.REALTIME, 1)) .thenReturn(Tasks.forResult(realtimeFetchedContainerResponse)); - configAutoFetch.listenForNotifications(); + configAutoFetch.listenForNotifications(sharedPrefsClient); verify(mockUnavailableEventListener, never()) .onError(any(FirebaseRemoteConfigServerException.class)); @@ -1546,11 +1546,29 @@ public void realtimeStreamListen_andUnableToParseMessage() throws Exception { when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber( ConfigFetchHandler.FetchType.REALTIME, 1)) .thenReturn(Tasks.forResult(realtimeFetchedContainerResponse)); - configAutoFetch.listenForNotifications(); + configAutoFetch.listenForNotifications(sharedPrefsClient); verify(mockInvalidMessageEventListener).onError(any(FirebaseRemoteConfigClientException.class)); } + @Test + public void realtime_updatesBackoffMetadataWithProvidedRetryInterval() throws Exception { + ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient); + when(mockHttpURLConnection.getResponseCode()).thenReturn(200); + int expectedRetryInterval = 240; + when(mockHttpURLConnection.getInputStream()) + .thenReturn( + new ByteArrayInputStream( + String.format( + "{ \"latestTemplateVersionNumber\": 1, \"retryIntervalSeconds\": %d }", + expectedRetryInterval) + .getBytes(StandardCharsets.UTF_8))); + when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L); + configAutoFetch.listenForNotifications(sharedPrefsClient); + + verify(sharedPrefsClient, times(1)).setRealtimeBackoffEndTime(any()); + } + @Test public void realtime_stream_listen_get_inputstream_fail() throws Exception { InputStream inputStream = mock(InputStream.class); @@ -1561,7 +1579,7 @@ public void realtime_stream_listen_get_inputstream_fail() throws Exception { when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber( ConfigFetchHandler.FetchType.REALTIME, 1)) .thenReturn(Tasks.forResult(realtimeFetchedContainerResponse)); - configAutoFetch.listenForNotifications(); + configAutoFetch.listenForNotifications(sharedPrefsClient); verify(inputStream).close(); } @@ -1571,7 +1589,7 @@ public void realtime_stream_listen_get_inputstream_exception_handling() throws E InputStream inputStream = mock(InputStream.class); when(mockHttpURLConnection.getResponseCode()).thenReturn(200); when(mockHttpURLConnection.getInputStream()).thenThrow(IOException.class); - configAutoFetch.listenForNotifications(); + configAutoFetch.listenForNotifications(sharedPrefsClient); verify(mockHttpURLConnection, times(1)).getInputStream(); verify(inputStream, never()).close();