Skip to content

Commit 7c6b17c

Browse files
cpovirkGoogle Java Core Libraries
authored andcommitted
Make FileBackedOutputStream use PhantomReference instead of finalize().
This generally makes it more resilient and future-proof, but we still don't recommend relying on GC for object cleanup, in part because any small change (like this one) could upset a delicate balance somewhere. Also, while I'm messing with `reachabilityFence` again: Remove a confused comment from 7eba58f in `GcFinalizationTest`: Once we use `reachabilityFence` directly, we'll eliminate the helper method, and http://errorprone.info/bugpattern/ReachabilityFenceUsage will remain happy because the call to `reachabilityFence` will appear directly inside a `finally` block. (I'm still not sure that `finally` is necessary there, but it's easy, and it's probably reasonable as a default practice.) (I'm also not sure if `finally` is necessary for the new usages of `reachabilityFence` in `FileBackedOutputStream`. I guess the fear would be that we'd allow the `FileOutputStream` to be closed while it's in the process of preparing to throw an exception, and the `close` call could clear something that the exception-construction code wants to read? I do suspect that the new `reachabilityFence` calls are a good idea to have _somewhere_, whether in a `finally` block or not, to ensure that background cleanup doesn't `reset` a `FileBackedOutputStream` during a call to a method like `write` (which might then open a _new_ file) or `close` (which might then not see an exception because the background `reset` sees it instead). Or maybe [`reachabilityFence` isn't necessary because our methods are `synchronized`](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/ref/Reference.html#reachabilityFence(java.lang.Object)). (`synchronized` should at least mean that we don't need the fence in a `finally` block, at least not for the reasons that I mentioned above: It won't be possible to `reset` the stream until `write` or `close` exits the critical area.) But presumably it won't hurt anything.) RELNOTES=n/a PiperOrigin-RevId: 875720195
1 parent 166a675 commit 7c6b17c

12 files changed

Lines changed: 604 additions & 448 deletions

File tree

android/guava-testlib/src/com/google/common/testing/GcFinalization.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ private static long timeoutSeconds() {
138138
*
139139
* @throws RuntimeException if timed out or interrupted while waiting
140140
*/
141-
@SuppressWarnings("removal") // b/260137033
141+
@SuppressWarnings("removal") // b/487687332
142142
public static void awaitDone(Future<?> future) {
143143
if (future.isDone()) {
144144
return;
@@ -171,7 +171,7 @@ public static void awaitDone(Future<?> future) {
171171
*
172172
* @throws RuntimeException if timed out or interrupted while waiting
173173
*/
174-
@SuppressWarnings("removal") // b/260137033
174+
@SuppressWarnings("removal") // b/487687332
175175
public static void awaitDone(FinalizationPredicate predicate) {
176176
if (predicate.isDone()) {
177177
return;
@@ -200,7 +200,7 @@ public static void awaitDone(FinalizationPredicate predicate) {
200200
*
201201
* @throws RuntimeException if timed out or interrupted while waiting
202202
*/
203-
@SuppressWarnings("removal") // b/260137033
203+
@SuppressWarnings("removal") // b/487687332
204204
public static void await(CountDownLatch latch) {
205205
if (latch.getCount() == 0) {
206206
return;
@@ -232,7 +232,7 @@ public static void await(CountDownLatch latch) {
232232
private static void createUnreachableLatchFinalizer(CountDownLatch latch) {
233233
Object unused =
234234
new Object() {
235-
@SuppressWarnings({"removal", "Finalize"}) // b/260137033
235+
@SuppressWarnings({"removal", "Finalize"}) // b/487687332
236236
@Override
237237
protected void finalize() {
238238
latch.countDown();
@@ -298,7 +298,7 @@ public static void awaitClear(WeakReference<?> ref) {
298298
* @throws RuntimeException if timed out or interrupted while waiting
299299
* @since 12.0
300300
*/
301-
@SuppressWarnings({"removal", "Finalize"}) // b/260137033
301+
@SuppressWarnings({"removal", "Finalize"}) // b/487687332
302302
public static void awaitFullGc() {
303303
CountDownLatch finalizerRan = new CountDownLatch(1);
304304
WeakReference<Object> ref =

android/guava-testlib/test/com/google/common/testing/GcFinalizationTest.java

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@
1616

1717
package com.google.common.testing;
1818

19-
import static com.google.common.testing.SneakyThrows.sneakyThrow;
2019
import static com.google.common.truth.Truth.assertThat;
2120
import static org.junit.Assert.assertThrows;
2221

23-
import com.google.common.testing.GcFinalization.FinalizationPredicate;
2422
import com.google.common.util.concurrent.SettableFuture;
2523
import java.lang.ref.Reference;
2624
import java.lang.ref.WeakReference;
2725
import java.lang.reflect.InvocationTargetException;
26+
import java.lang.reflect.Method;
2827
import java.util.WeakHashMap;
2928
import java.util.concurrent.CountDownLatch;
3029
import java.util.concurrent.atomic.AtomicBoolean;
@@ -51,7 +50,7 @@ public void testAwait_countDownLatch() {
5150
CountDownLatch latch = new CountDownLatch(1);
5251
Object unused =
5352
new Object() {
54-
@SuppressWarnings({"removal", "Finalize"}) // b/260137033
53+
@SuppressWarnings({"removal", "Finalize"}) // b/487687332
5554
@Override
5655
protected void finalize() {
5756
latch.countDown();
@@ -66,7 +65,7 @@ public void testAwaitDone_future() {
6665
SettableFuture<@Nullable Void> future = SettableFuture.create();
6766
Object unused =
6867
new Object() {
69-
@SuppressWarnings({"removal", "Finalize"}) // b/260137033
68+
@SuppressWarnings({"removal", "Finalize"}) // b/487687332
7069
@Override
7170
protected void finalize() {
7271
future.set(null);
@@ -82,7 +81,7 @@ public void testAwaitDone_future_cancel() {
8281
SettableFuture<@Nullable Void> future = SettableFuture.create();
8382
Object unused =
8483
new Object() {
85-
@SuppressWarnings({"removal", "Finalize"}) // b/260137033
84+
@SuppressWarnings({"removal", "Finalize"}) // b/487687332
8685
@Override
8786
protected void finalize() {
8887
future.cancel(false);
@@ -103,13 +102,7 @@ public void testAwaitClear() {
103102
public void testAwaitDone_finalizationPredicate() {
104103
WeakHashMap<Object, Object> map = new WeakHashMap<>();
105104
map.put(new Object(), Boolean.TRUE);
106-
GcFinalization.awaitDone(
107-
new FinalizationPredicate() {
108-
@Override
109-
public boolean isDone() {
110-
return map.isEmpty();
111-
}
112-
});
105+
GcFinalization.awaitDone(map::isEmpty);
113106
assertTrue(map.isEmpty());
114107
}
115108

@@ -202,16 +195,7 @@ public void testAwaitDone_finalizationPredicate_interrupted() {
202195
Interruptenator interruptenator = new Interruptenator(Thread.currentThread());
203196
try {
204197
RuntimeException expected =
205-
assertThrows(
206-
RuntimeException.class,
207-
() ->
208-
GcFinalization.awaitDone(
209-
new FinalizationPredicate() {
210-
@Override
211-
public boolean isDone() {
212-
return false;
213-
}
214-
}));
198+
assertThrows(RuntimeException.class, () -> GcFinalization.awaitDone(() -> false));
215199
assertWrapsInterruptedException(expected);
216200
} finally {
217201
interruptenator.shutdown();
@@ -227,9 +211,9 @@ public boolean isDone() {
227211
public void testAwaitFullGc() {
228212
CountDownLatch finalizerRan = new CountDownLatch(1);
229213
WeakReference<Object> ref =
230-
new WeakReference<Object>(
214+
new WeakReference<>(
231215
new Object() {
232-
@SuppressWarnings({"removal", "Finalize"}) // b/260137033
216+
@SuppressWarnings({"removal", "Finalize"}) // b/487687332
233217
@Override
234218
protected void finalize() {
235219
finalizerRan.countDown();
@@ -247,26 +231,35 @@ protected void finalize() {
247231
assertThat(ref.get()).isNull();
248232
}
249233

250-
/*
251-
* Once we can call Reference.reachabilityFence() directly, we will need to suppress
252-
* https://errorprone.info/bugpattern/ReachabilityFenceUsage. Suppressing is fine: This method is a wrapper around that
253-
* method, and we call that wrapper from a `finally` block, as the check wants. (Also, it's not
254-
* entirely clear to me that we really need to use `finally` in this case: b/127970178.)
255-
*/
256-
private static void reachabilityFence(Object o) {
234+
// We call the method only after checking that it's present.
235+
@IgnoreJRERequirement
236+
@SuppressWarnings({
237+
"Java8ApiChecker",
238+
// This method is a helper, which we call from a `finally` block, as recommended.
239+
"ReachabilityFenceUsage",
240+
})
241+
static void reachabilityFence(@Nullable Object o) {
242+
if (IS_REACHABILITY_FENCE_METHOD_USABLE) {
243+
Reference.reachabilityFence(o);
244+
}
245+
}
246+
247+
private static final boolean IS_REACHABILITY_FENCE_METHOD_USABLE =
248+
computeIsReachabilityFenceMethodUsable();
249+
250+
private static boolean computeIsReachabilityFenceMethodUsable() {
257251
try {
258-
Reference.class.getMethod("reachabilityFence", Object.class).invoke(null, o);
259-
} catch (InvocationTargetException e) {
260-
// The method has no `throws` clause.
261-
sneakyThrow(e.getCause());
262-
} catch (IllegalAccessException e) {
252+
Method method = Reference.class.getMethod("reachabilityFence", Object.class);
253+
method.invoke(null, GcFinalizationTest.class); // to make sure the method is accessible
254+
return true;
255+
} catch (NoSuchMethodException | IllegalAccessException probablyBeforeJava9OrAndroid28) {
263256
/*
264-
* The method should be public (though technically there's nothing preventing it from being
265-
* present as non-public under older versions).
257+
* It's theoretically possible for Reference.reachabilityFence to exist under older VMs in an
258+
* inaccessible form.
266259
*/
267-
throw new LinkageError(e.toString(), e);
268-
} catch (NoSuchMethodException e) {
269-
// We're running under Java 8 or under Android before API Level 28.
260+
return false;
261+
} catch (InvocationTargetException e) {
262+
throw new AssertionError(e.getCause());
270263
}
271264
}
272265
}

android/guava-testlib/test/com/google/common/testing/SneakyThrows.java

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

android/guava-tests/test/com/google/common/io/FileBackedOutputStreamTest.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
package com.google.common.io;
1818

1919
import static com.google.common.base.StandardSystemProperty.OS_NAME;
20+
import static com.google.common.io.FileBackedOutputStream.reachabilityFence;
2021
import static com.google.common.primitives.Bytes.concat;
22+
import static com.google.common.testing.GcFinalization.awaitClear;
23+
import static com.google.common.testing.GcFinalization.awaitDone;
2124
import static com.google.common.truth.Truth.assertThat;
2225
import static java.lang.Math.min;
2326
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
@@ -27,6 +30,7 @@
2730
import java.io.File;
2831
import java.io.IOException;
2932
import java.io.OutputStream;
33+
import java.lang.ref.WeakReference;
3034
import java.nio.file.attribute.PosixFileAttributeView;
3135
import java.nio.file.attribute.PosixFileAttributes;
3236
import org.jspecify.annotations.NullUnmarked;
@@ -54,10 +58,11 @@ public void testThreshold() throws Exception {
5458
}
5559

5660
private void testThreshold(
57-
int fileThreshold, int dataSize, boolean singleByte, boolean resetOnFinalize)
61+
int fileThreshold, int dataSize, boolean singleByte, boolean resetWhenGarbageCollected)
5862
throws IOException {
5963
byte[] data = newPreFilledByteArray(dataSize);
60-
FileBackedOutputStream out = new FileBackedOutputStream(fileThreshold, resetOnFinalize);
64+
FileBackedOutputStream out =
65+
new FileBackedOutputStream(fileThreshold, resetWhenGarbageCollected);
6166
ByteSource source = out.asByteSource();
6267
int chunk1 = min(dataSize, fileThreshold);
6368
int chunk2 = dataSize - chunk1;
@@ -97,7 +102,7 @@ private void testThreshold(
97102
}
98103

99104

100-
public void testThreshold_resetOnFinalize() throws Exception {
105+
public void testThreshold_resetWhenGarbageCollected() throws Exception {
101106
testThreshold(0, 100, true, true);
102107
testThreshold(10, 100, true, true);
103108
testThreshold(100, 100, true, true);
@@ -197,4 +202,20 @@ public void testThresholdCrossing_resourceManagement() throws Exception {
197202

198203
out.reset();
199204
}
205+
206+
207+
@AndroidIncompatible // depends on details of GC
208+
public void testResetWhenGarbageCollected() throws Exception {
209+
FileBackedOutputStream out =
210+
new FileBackedOutputStream(0, /* resetWhenGarbageCollected= */ true);
211+
out.write(new byte[10]);
212+
File file = out.getFile();
213+
assertThat(file.exists()).isTrue();
214+
reachabilityFence(out); // not sure if this is necessary even in theory
215+
216+
WeakReference<ByteSource> sourceRef = new WeakReference<>(out.asByteSource());
217+
out = null;
218+
awaitClear(sourceRef);
219+
awaitDone(() -> !file.exists());
220+
}
200221
}

0 commit comments

Comments
 (0)