diff --git a/android/guava-tests/test/com/google/common/util/concurrent/FuturesTest.java b/android/guava-tests/test/com/google/common/util/concurrent/FuturesTest.java index 048dbc778868..089fa1dc6676 100644 --- a/android/guava-tests/test/com/google/common/util/concurrent/FuturesTest.java +++ b/android/guava-tests/test/com/google/common/util/concurrent/FuturesTest.java @@ -2801,6 +2801,35 @@ public ListenableFuture call() throws Exception { } } + @AndroidIncompatible + @GwtIncompatible + public void testWhenAllSucceed_releasesMemory() throws Exception { + SettableFuture future1 = SettableFuture.create(); + SettableFuture future2 = SettableFuture.create(); + WeakReference> future1Ref = new WeakReference<>(future1); + WeakReference> future2Ref = new WeakReference<>(future2); + + AsyncCallable combiner = + new AsyncCallable() { + @Override + public ListenableFuture call() throws Exception { + return SettableFuture.create(); + } + }; + + ListenableFuture unused = + whenAllSucceed(future1, future2).callAsync(combiner, directExecutor()); + + future1.set(1L); + future1 = null; + future2.set(2L); + future2 = null; + + // Futures should be collected even if combiner future never finishes. + GcFinalization.awaitClear(future1Ref); + GcFinalization.awaitClear(future2Ref); + } + /* * TODO(cpovirk): maybe pass around TestFuture instances instead of * ListenableFuture instances diff --git a/android/guava/src/com/google/common/util/concurrent/AggregateFuture.java b/android/guava/src/com/google/common/util/concurrent/AggregateFuture.java index c7827d15354c..18e0e9e898b2 100644 --- a/android/guava/src/com/google/common/util/concurrent/AggregateFuture.java +++ b/android/guava/src/com/google/common/util/concurrent/AggregateFuture.java @@ -49,6 +49,10 @@ abstract class AggregateFuture extends AbstractFuture.TrustedFu @Override protected final void afterDone() { super.afterDone(); + releaseResources(); + } + + protected final void releaseResources() { RunningState localRunningState = runningState; if (localRunningState != null) { // Let go of the memory held by the running state diff --git a/android/guava/src/com/google/common/util/concurrent/CombinedFuture.java b/android/guava/src/com/google/common/util/concurrent/CombinedFuture.java index da9d539e32c5..ded4b77b214f 100644 --- a/android/guava/src/com/google/common/util/concurrent/CombinedFuture.java +++ b/android/guava/src/com/google/common/util/concurrent/CombinedFuture.java @@ -157,6 +157,9 @@ ListenableFuture runInterruptibly() throws Exception { @Override void setValue(ListenableFuture value) { setFuture(value); + // Eagerly release resources instead of waiting for afterDone. We are done with the inputs, + // but the actual future may not complete for arbitrarily long. + releaseResources(); } @Override diff --git a/guava-tests/test/com/google/common/util/concurrent/FuturesTest.java b/guava-tests/test/com/google/common/util/concurrent/FuturesTest.java index 33744b2cde02..571bed062dde 100644 --- a/guava-tests/test/com/google/common/util/concurrent/FuturesTest.java +++ b/guava-tests/test/com/google/common/util/concurrent/FuturesTest.java @@ -2801,6 +2801,35 @@ public ListenableFuture call() throws Exception { } } + @AndroidIncompatible + @GwtIncompatible + public void testWhenAllSucceed_releasesMemory() throws Exception { + SettableFuture future1 = SettableFuture.create(); + SettableFuture future2 = SettableFuture.create(); + WeakReference> future1Ref = new WeakReference<>(future1); + WeakReference> future2Ref = new WeakReference<>(future2); + + AsyncCallable combiner = + new AsyncCallable() { + @Override + public ListenableFuture call() throws Exception { + return SettableFuture.create(); + } + }; + + ListenableFuture unused = + whenAllSucceed(future1, future2).callAsync(combiner, directExecutor()); + + future1.set(1L); + future1 = null; + future2.set(2L); + future2 = null; + + // Futures should be collected even if combiner future never finishes. + GcFinalization.awaitClear(future1Ref); + GcFinalization.awaitClear(future2Ref); + } + /* * TODO(cpovirk): maybe pass around TestFuture instances instead of * ListenableFuture instances diff --git a/guava/src/com/google/common/util/concurrent/AggregateFuture.java b/guava/src/com/google/common/util/concurrent/AggregateFuture.java index 432684578a24..90670271e9ea 100644 --- a/guava/src/com/google/common/util/concurrent/AggregateFuture.java +++ b/guava/src/com/google/common/util/concurrent/AggregateFuture.java @@ -49,6 +49,10 @@ abstract class AggregateFuture extends AbstractFuture.TrustedFu @Override protected final void afterDone() { super.afterDone(); + releaseResources(); + } + + protected final void releaseResources() { RunningState localRunningState = runningState; if (localRunningState != null) { // Let go of the memory held by the running state diff --git a/guava/src/com/google/common/util/concurrent/CombinedFuture.java b/guava/src/com/google/common/util/concurrent/CombinedFuture.java index 7755e0e90da3..c50fc0bc7753 100644 --- a/guava/src/com/google/common/util/concurrent/CombinedFuture.java +++ b/guava/src/com/google/common/util/concurrent/CombinedFuture.java @@ -157,6 +157,9 @@ ListenableFuture runInterruptibly() throws Exception { @Override void setValue(ListenableFuture value) { setFuture(value); + // Eagerly release resources instead of waiting for afterDone. We are done with the inputs, + // but the actual future may not complete for arbitrarily long. + releaseResources(); } @Override