Skip to content

Commit afecb8c

Browse files
feat: add toString to futures returned by operations (googleapis#3140)
Sometimes an operation can get stuck indefinitely. The underlying reasons can vary significantly: - the underlying attempt rpc can get stuck due to a bug in grpc (ie grpc/grpc-java#11026) - the operation can get stuck in layers above gax: googleapis/java-bigtable#1939 - or it can get stuck in gax itself (dont have a pointer handy) Guava futures provide some observability for ListenableFutures, but in creating the custom ApiFutures in gax, we lose that functionality. This PR sprinkles a few to toString to allow callers to inspect the internal state of the operation. For example with these changes, the toString() of the future returned from bigtableDataClient.mutateRows() changes from > TransformFuture@652ce654[status=PENDING, info=[inputFuture=[com.google.api.core.ApiFutureToListenableFuture@522ba524], function=[com.google.api.core.ApiFutures$ApiFunctionToGuavaFunction@29c5ee1d]]] to > ListenableFutureToApiFuture{delegate=TransformFuture@7ac9af2a[status=PENDING, info=[inputFuture=[ApiFutureToListenableFuture{apiFuture=CallbackChainRetryingFuture{super=com.google.api.gax.retrying.CallbackChainRetryingFuture@7bb004b8[status=PENDING], latestCompletedAttemptResult=null, attemptResult=null, attemptSettings=TimedAttemptSettings{globalSettings=RetrySettings{totalTimeout=PT10M, initialRetryDelay=PT0.01S, retryDelayMultiplier=2.0, maxRetryDelay=PT1M, maxAttempts=0, jittered=true, initialRpcTimeout=PT1M, rpcTimeoutMultiplier=1.0, maxRpcTimeout=PT1M}, retryDelay=PT0S, rpcTimeout=PT1M, randomizedRetryDelay=PT0S, attemptCount=0, overallAttemptCount=0, firstAttemptStartTimeNanos=635709620001791}}}], function=[com.google.api.core.ApiFutures$ApiFunctionToGuavaFunction@652ce654]]]} This allows us to reason about whats stuck. I'm working another PR that will add a close(timeout) to the Batcher that will use this functionality to identify why batcher.close() timed out
1 parent cb24fdb commit afecb8c

File tree

5 files changed

+95
-0
lines changed

5 files changed

+95
-0
lines changed

api-common-java/src/main/java/com/google/api/core/ApiFutureToListenableFuture.java

+8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
*/
3030
package com.google.api.core;
3131

32+
import com.google.common.base.MoreObjects;
3233
import com.google.common.util.concurrent.ListenableFuture;
3334
import java.util.concurrent.ExecutionException;
3435
import java.util.concurrent.Executor;
@@ -74,4 +75,11 @@ public V get(long l, TimeUnit timeUnit)
7475
throws InterruptedException, ExecutionException, TimeoutException {
7576
return apiFuture.get(l, timeUnit);
7677
}
78+
79+
@Override
80+
public String toString() {
81+
return MoreObjects.toStringHelper(ApiFutureToListenableFuture.class.getSimpleName())
82+
.add("apiFuture", apiFuture)
83+
.toString();
84+
}
7785
}

api-common-java/src/main/java/com/google/api/core/ListenableFutureToApiFuture.java

+7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
*/
3030
package com.google.api.core;
3131

32+
import com.google.common.base.MoreObjects;
3233
import com.google.common.util.concurrent.ForwardingListenableFuture.SimpleForwardingListenableFuture;
3334
import com.google.common.util.concurrent.ListenableFuture;
3435

@@ -39,4 +40,10 @@ public class ListenableFutureToApiFuture<V> extends SimpleForwardingListenableFu
3940
public ListenableFutureToApiFuture(ListenableFuture<V> delegate) {
4041
super(delegate);
4142
}
43+
44+
public String toString() {
45+
return MoreObjects.toStringHelper(ListenableFutureToApiFuture.class)
46+
.add("delegate", delegate())
47+
.toString();
48+
}
4249
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2024, Google Inc.
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google Inc. nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.core;
31+
32+
import static com.google.common.truth.Truth.assertThat;
33+
34+
import org.junit.jupiter.api.Test;
35+
36+
class ApiFutureToListenableFutureTest {
37+
@Test
38+
void testThatInnerToStringIsNotLost() {
39+
String customInnerToString = "my-custom-inner-tostring";
40+
ApiFuture<String> apiFuture =
41+
new AbstractApiFuture<String>() {
42+
@Override
43+
public String toString() {
44+
return customInnerToString;
45+
}
46+
};
47+
ApiFutureToListenableFuture<String> listenableFuture =
48+
new ApiFutureToListenableFuture<>(apiFuture);
49+
assertThat(listenableFuture.toString()).contains(customInnerToString);
50+
}
51+
}

api-common-java/src/test/java/com/google/api/core/ListenableFutureToApiFutureTest.java

+18
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
package com.google.api.core;
3131

3232
import com.google.common.truth.Truth;
33+
import com.google.common.util.concurrent.AbstractFuture;
34+
import com.google.common.util.concurrent.ListenableFuture;
3335
import com.google.common.util.concurrent.SettableFuture;
3436
import org.junit.jupiter.api.Test;
3537

@@ -42,4 +44,20 @@ void testGet() throws Exception {
4244
future.set(3);
4345
Truth.assertThat(apiFuture.get()).isEqualTo(3);
4446
}
47+
48+
@Test
49+
void testToStringShowsUnderlyingFutureToString() {
50+
String customInnerFutureDesc = "my-custom-toString-impl";
51+
ListenableFuture<String> listenableFuture =
52+
new AbstractFuture<String>() {
53+
@Override
54+
public String toString() {
55+
return customInnerFutureDesc;
56+
}
57+
};
58+
59+
ListenableFutureToApiFuture<String> apiFuture =
60+
new ListenableFutureToApiFuture<>(listenableFuture);
61+
Truth.assertThat(apiFuture.toString()).contains(customInnerFutureDesc);
62+
}
4563
}

gax-java/gax/src/main/java/com/google/api/gax/retrying/BasicRetryingFuture.java

+11
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import com.google.api.core.ApiFuture;
3636
import com.google.api.core.ApiFutures;
3737
import com.google.api.gax.tracing.ApiTracer;
38+
import com.google.common.base.MoreObjects;
3839
import com.google.common.util.concurrent.AbstractFuture;
3940
import com.google.common.util.concurrent.MoreExecutors;
4041
import java.util.concurrent.Callable;
@@ -265,6 +266,16 @@ private void setAttemptResult(Throwable throwable, ResponseT response, boolean s
265266
}
266267
}
267268

269+
@Override
270+
public String toString() {
271+
return MoreObjects.toStringHelper(this.getClass())
272+
.add("super", pendingToString())
273+
.add("latestCompletedAttemptResult", this.latestCompletedAttemptResult)
274+
.add("attemptResult", this.attemptResult)
275+
.add("attemptSettings", this.attemptSettings)
276+
.toString();
277+
}
278+
268279
private class CompletionListener implements Runnable {
269280
@Override
270281
public void run() {

0 commit comments

Comments
 (0)