Skip to content

Commit c0bca78

Browse files
committed
Merge remote-tracking branch 'origin/main' into MutableStateFlowUseUpdateInsteadOfCompareAndSet
2 parents e9b91a7 + d7a56a1 commit c0bca78

File tree

13 files changed

+230
-102
lines changed

13 files changed

+230
-102
lines changed

.github/workflows/fireci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ jobs:
2424
- run: |
2525
pytest ci/fireci
2626
- run: |
27-
mypy --config-file ci/fireci/setup.cfg ci/fireci/
27+
mypy --config-file ci/fireci/pyproject.toml ci/fireci/

ci/fireci/pyproject.toml

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,44 @@
11
[build-system]
2-
requires = ["setuptools ~= 58.0"]
2+
requires = ["setuptools ~= 70.0"]
33
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "fireci"
7+
version = "0.1"
8+
dependencies = [
9+
"protobuf==3.20.3",
10+
"click==8.1.7",
11+
"google-cloud-storage==2.18.2",
12+
"mypy==1.6.0",
13+
"numpy==1.24.4",
14+
"pandas==1.5.3",
15+
"PyGithub==1.58.2",
16+
"pystache==0.6.0",
17+
"requests==2.31.0",
18+
"seaborn==0.12.2",
19+
"PyYAML==6.0.1",
20+
"termcolor==2.4.0",
21+
"pytest"
22+
]
23+
24+
[project.scripts]
25+
fireci = "fireci.main:cli"
26+
27+
[tool.setuptools]
28+
packages = ["fireci", "fireciplugins"]
29+
30+
[tool.mypy]
31+
strict_optional = false
32+
33+
[[tool.mypy.overrides]]
34+
module = [
35+
"google.cloud",
36+
"matplotlib",
37+
"matplotlib.pyplot",
38+
"pandas",
39+
"pystache",
40+
"requests",
41+
"seaborn",
42+
"yaml"
43+
]
44+
ignore_missing_imports = true

ci/fireci/setup.cfg

Lines changed: 0 additions & 45 deletions
This file was deleted.

ci/run.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
# limitations under the License.
1616

1717
set -e
18+
set -x
1819

1920
DIRECTORY=$(cd `dirname $0` && pwd)
20-
pip3 install -e $DIRECTORY/fireci >> /dev/null
21+
python3 -m ensurepip --upgrade
22+
python3 -m pip install --upgrade setuptools
23+
python3 -m pip install --upgrade pip
24+
python3 -m pip install --upgrade wheel
25+
python3 -m pip install -e $DIRECTORY/fireci >> /dev/null
2126
fireci $@

firebase-config/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Unreleased
2-
2+
[fixed] Fixed an issue where the connection to the real-time Remote Config backend could remain
3+
open in the background.
34

45
# 22.1.0
56
* [feature] Added support for custom signal targeting in Remote Config. Use `setCustomSignals` API for setting custom signals and use them to build custom targeting conditions in Remote Config.

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

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class ConfigAutoFetch {
5454
private final ConfigUpdateListener retryCallback;
5555
private final ScheduledExecutorService scheduledExecutorService;
5656
private final Random random;
57+
private boolean isInBackground;
5758

5859
public ConfigAutoFetch(
5960
HttpURLConnection httpURLConnection,
@@ -69,6 +70,7 @@ public ConfigAutoFetch(
6970
this.retryCallback = retryCallback;
7071
this.scheduledExecutorService = scheduledExecutorService;
7172
this.random = new Random();
73+
this.isInBackground = false;
7274
}
7375

7476
private synchronized void propagateErrors(FirebaseRemoteConfigException exception) {
@@ -87,6 +89,10 @@ private synchronized boolean isEventListenersEmpty() {
8789
return this.eventListeners.isEmpty();
8890
}
8991

92+
public void setIsInBackground(boolean isInBackground) {
93+
this.isInBackground = isInBackground;
94+
}
95+
9096
private String parseAndValidateConfigUpdateMessage(String message) {
9197
int left = message.indexOf('{');
9298
int right = message.lastIndexOf('}');
@@ -105,15 +111,29 @@ public void listenForNotifications() {
105111
return;
106112
}
107113

114+
// Maintain a reference to the InputStream to guarantee its closure upon completion or in case
115+
// of an exception.
116+
InputStream inputStream = null;
108117
try {
109-
InputStream inputStream = httpURLConnection.getInputStream();
118+
inputStream = httpURLConnection.getInputStream();
110119
handleNotifications(inputStream);
111-
inputStream.close();
112120
} catch (IOException ex) {
113-
// Stream was interrupted due to a transient issue and the system will retry the connection.
114-
Log.d(TAG, "Stream was cancelled due to an exception. Retrying the connection...", ex);
121+
// If the real-time connection is at an unexpected lifecycle state when the app is
122+
// backgrounded, it's expected closing the httpURLConnection will throw an exception.
123+
if (!isInBackground) {
124+
// Otherwise, the real-time server connection was closed due to a transient issue.
125+
Log.d(TAG, "Real-time connection was closed due to an exception.", ex);
126+
}
115127
} finally {
116-
httpURLConnection.disconnect();
128+
if (inputStream != null) {
129+
try {
130+
// Only need to close the InputStream, ConfigRealtimeHttpClient will disconnect
131+
// HttpUrlConnection
132+
inputStream.close();
133+
} catch (IOException ex) {
134+
Log.d(TAG, "Exception thrown when closing connection stream. Retrying connection...", ex);
135+
}
136+
}
117137
}
118138
}
119139

@@ -186,7 +206,6 @@ private void handleNotifications(InputStream inputStream) throws IOException {
186206
}
187207

188208
reader.close();
189-
inputStream.close();
190209
}
191210

192211
private void autoFetch(int remainingAttempts, long targetVersion) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public synchronized ConfigUpdateListenerRegistration addRealtimeConfigUpdateList
9191
}
9292

9393
public synchronized void setBackgroundState(boolean isInBackground) {
94-
configRealtimeHttpClient.setRealtimeBackgroundState(isInBackground);
94+
configRealtimeHttpClient.setIsInBackground(isInBackground);
9595
if (!isInBackground) {
9696
beginRealtime();
9797
}

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

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ public class ConfigRealtimeHttpClient {
100100
/** Flag to indicate whether or not the app is in the background or not. */
101101
private boolean isInBackground;
102102

103+
// The HttpUrlConnection and auto-fetcher for this client. Only one of each exist at a time.
104+
private HttpURLConnection httpURLConnection;
105+
private ConfigAutoFetch configAutoFetch;
106+
103107
private final int ORIGINAL_RETRIES = 8;
104108
private final ScheduledExecutorService scheduledExecutorService;
105109
private final ConfigFetchHandler configFetchHandler;
@@ -111,6 +115,7 @@ public class ConfigRealtimeHttpClient {
111115
private final Random random;
112116
private final Clock clock;
113117
private final ConfigSharedPrefsClient sharedPrefsClient;
118+
private final Object backgroundLock;
114119

115120
public ConfigRealtimeHttpClient(
116121
FirebaseApp firebaseApp,
@@ -145,6 +150,7 @@ public ConfigRealtimeHttpClient(
145150
this.sharedPrefsClient = sharedPrefsClient;
146151
this.isRealtimeDisabled = false;
147152
this.isInBackground = false;
153+
this.backgroundLock = new Object();
148154
}
149155

150156
private static String extractProjectNumberFromAppId(String gmpAppId) {
@@ -391,14 +397,42 @@ public void run() {
391397
}
392398
}
393399

394-
void setRealtimeBackgroundState(boolean backgroundState) {
395-
isInBackground = backgroundState;
400+
public void setIsInBackground(boolean isInBackground) {
401+
// Make changes in synchronized block so only one thread sets the background state and calls
402+
// disconnect.
403+
synchronized (backgroundLock) {
404+
this.isInBackground = isInBackground;
405+
406+
// Propagate to ConfigAutoFetch as well.
407+
if (configAutoFetch != null) {
408+
configAutoFetch.setIsInBackground(isInBackground);
409+
}
410+
// Close the connection if the app is in the background and there is an active
411+
// HttpUrlConnection.
412+
if (isInBackground && httpURLConnection != null) {
413+
httpURLConnection.disconnect();
414+
}
415+
}
396416
}
397417

398418
private synchronized void resetRetryCount() {
399419
httpRetriesRemaining = ORIGINAL_RETRIES;
400420
}
401421

422+
/**
423+
* The check and set http connection method are combined so that when canMakeHttpStreamConnection
424+
* returns true, the same thread can mark isHttpConnectionIsRunning as true to prevent a race
425+
* condition with another thread.
426+
*/
427+
private synchronized boolean checkAndSetHttpConnectionFlagIfNotRunning() {
428+
boolean canMakeConnection = canMakeHttpStreamConnection();
429+
if (canMakeConnection) {
430+
setIsHttpConnectionRunning(true);
431+
}
432+
433+
return canMakeConnection;
434+
}
435+
402436
private synchronized void setIsHttpConnectionRunning(boolean connectionRunning) {
403437
isHttpConnectionRunning = connectionRunning;
404438
}
@@ -469,7 +503,7 @@ private String parseForbiddenErrorResponseMessage(InputStream inputStream) {
469503
*/
470504
@SuppressLint({"VisibleForTests", "DefaultLocale"})
471505
public void beginRealtimeHttpStream() {
472-
if (!canMakeHttpStreamConnection()) {
506+
if (!checkAndSetHttpConnectionFlagIfNotRunning()) {
473507
return;
474508
}
475509

@@ -489,17 +523,21 @@ public void beginRealtimeHttpStream() {
489523
this.scheduledExecutorService,
490524
(completedHttpUrlConnectionTask) -> {
491525
Integer responseCode = null;
492-
HttpURLConnection httpURLConnection = null;
526+
// Get references to InputStream and ErrorStream before listening on the stream so
527+
// that they can be closed without getting them from HttpUrlConnection.
528+
InputStream inputStream = null;
529+
InputStream errorStream = null;
493530

494531
try {
495532
// If HTTP connection task failed throw exception to move to the catch block.
496533
if (!httpURLConnectionTask.isSuccessful()) {
497534
throw new IOException(httpURLConnectionTask.getException());
498535
}
499-
setIsHttpConnectionRunning(true);
500536

501537
// Get HTTP connection and check response code.
502538
httpURLConnection = httpURLConnectionTask.getResult();
539+
inputStream = httpURLConnection.getInputStream();
540+
errorStream = httpURLConnection.getErrorStream();
503541
responseCode = httpURLConnection.getResponseCode();
504542

505543
// If the connection returned a 200 response code, start listening for messages.
@@ -509,23 +547,32 @@ public void beginRealtimeHttpStream() {
509547
sharedPrefsClient.resetRealtimeBackoff();
510548

511549
// Start listening for realtime notifications.
512-
ConfigAutoFetch configAutoFetch = startAutoFetch(httpURLConnection);
550+
configAutoFetch = startAutoFetch(httpURLConnection);
513551
configAutoFetch.listenForNotifications();
514552
}
515553
} catch (IOException e) {
516-
// Stream could not be open due to a transient issue and the system will retry the
517-
// connection
518-
// without user intervention.
519-
Log.d(
520-
TAG,
521-
"Exception connecting to real-time RC backend. Retrying the connection...",
522-
e);
554+
if (isInBackground) {
555+
// It's possible the app was backgrounded while the connection was open, which
556+
// threw an exception trying to read the response. No real error here, so treat
557+
// this as a success, even if we haven't read a 200 response code yet.
558+
resetRetryCount();
559+
} else {
560+
// If it's not in the background, there might have been a transient error so the
561+
// client will retry the connection.
562+
Log.d(
563+
TAG,
564+
"Exception connecting to real-time RC backend. Retrying the connection...",
565+
e);
566+
}
523567
} finally {
524-
closeRealtimeHttpStream(httpURLConnection);
568+
// Close HTTP connection and associated streams.
569+
closeRealtimeHttpConnection(inputStream, errorStream);
525570
setIsHttpConnectionRunning(false);
526571

572+
// Update backoff metadata if the connection failed in the foreground.
527573
boolean connectionFailed =
528-
responseCode == null || isStatusCodeRetryable(responseCode);
574+
!isInBackground
575+
&& (responseCode == null || isStatusCodeRetryable(responseCode));
529576
if (connectionFailed) {
530577
updateBackoffMetadataWithLastFailedStreamConnectionTime(
531578
new Date(clock.currentTimeMillis()));
@@ -556,24 +603,34 @@ public void beginRealtimeHttpStream() {
556603
}
557604
}
558605

606+
// Reset parameters.
607+
httpURLConnection = null;
608+
configAutoFetch = null;
609+
559610
return Tasks.forResult(null);
560611
});
561612
}
562613

563-
// Pauses Http stream listening
564-
public void closeRealtimeHttpStream(HttpURLConnection httpURLConnection) {
565-
if (httpURLConnection != null) {
566-
httpURLConnection.disconnect();
567-
568-
// Explicitly close the input stream due to a bug in the Android okhttp implementation.
569-
// See github.com/firebase/firebase-android-sdk/pull/808.
614+
private void closeHttpConnectionInputStream(InputStream inputStream) {
615+
if (inputStream != null) {
570616
try {
571-
httpURLConnection.getInputStream().close();
572-
if (httpURLConnection.getErrorStream() != null) {
573-
httpURLConnection.getErrorStream().close();
574-
}
575-
} catch (IOException e) {
617+
inputStream.close();
618+
} catch (IOException ex) {
619+
Log.d(TAG, "Error closing connection stream.", ex);
576620
}
577621
}
578622
}
623+
624+
// Pauses Http stream listening by disconnecting the HttpUrlConnection and underlying InputStream
625+
// and ErrorStream if they exist.
626+
@VisibleForTesting
627+
public void closeRealtimeHttpConnection(InputStream inputStream, InputStream errorStream) {
628+
// Disconnect only if the connection is not null and in the foreground.
629+
if (httpURLConnection != null && !isInBackground) {
630+
httpURLConnection.disconnect();
631+
}
632+
633+
closeHttpConnectionInputStream(inputStream);
634+
closeHttpConnectionInputStream(errorStream);
635+
}
579636
}

0 commit comments

Comments
 (0)