diff --git a/gax-java/gax/src/main/java/com/google/api/gax/batching/BatcherStats.java b/gax-java/gax/src/main/java/com/google/api/gax/batching/BatcherStats.java index 784af6f599..20b0f95954 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/batching/BatcherStats.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/batching/BatcherStats.java @@ -31,7 +31,9 @@ import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.StatusCode.Code; +import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; +import com.google.common.collect.EvictingQueue; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -52,6 +54,21 @@ class BatcherStats { private final Map entryExceptionCounts = new HashMap<>(); private final Map entryStatusCounts = new HashMap<>(); + /** + * The maximum number of error messages that a Batcher instance will retain. By default, a Batcher + * instance will retain 50 entry error messages and 50 RPC error messages. This limit can be + * temporarily increased by setting the {@code com.google.api.gax.batching.errors.max-samples} + * system property. This should only be needed in very rare situations and should not be + * considered part of the public api. + */ + private final int MAX_ERROR_MSG_SAMPLES = + Integer.getInteger("com.google.api.gax.batching.errors.max-samples", 50); + + private final EvictingQueue sampleOfRpcErrors = + EvictingQueue.create(MAX_ERROR_MSG_SAMPLES); + private final EvictingQueue sampleOfEntryErrors = + EvictingQueue.create(MAX_ERROR_MSG_SAMPLES); + /** * Records the count of the exception and it's type when a complete batch failed to apply. * @@ -69,6 +86,8 @@ synchronized void recordBatchFailure(Throwable throwable) { requestStatusCounts.put(code, oldStatusCount + 1); } + sampleOfRpcErrors.add(throwable.toString()); + int oldExceptionCount = MoreObjects.firstNonNull(requestExceptionCounts.get(exceptionClass), 0); requestExceptionCounts.put(exceptionClass, oldExceptionCount + 1); } @@ -96,6 +115,8 @@ synchronized void recordBatchElementsCompletion( Throwable actualCause = throwable.getCause(); Class exceptionClass = actualCause.getClass(); + sampleOfEntryErrors.add(actualCause.toString()); + if (actualCause instanceof ApiException) { Code code = ((ApiException) actualCause).getStatusCode().getCode(); exceptionClass = ApiException.class; @@ -144,6 +165,17 @@ synchronized BatchingException asException() { .append(buildExceptionList(entryExceptionCounts, entryStatusCounts)) .append("."); } + + if (!sampleOfRpcErrors.isEmpty()) { + messageBuilder.append(" Sample of RPC errors: "); + messageBuilder.append(Joiner.on(", ").join(sampleOfRpcErrors)); + messageBuilder.append("."); + } + if (!sampleOfEntryErrors.isEmpty()) { + messageBuilder.append(" Sample of entry errors: "); + messageBuilder.append(Joiner.on(", ").join(sampleOfEntryErrors)); + messageBuilder.append("."); + } return new BatchingException(messageBuilder.toString()); } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/batching/BatcherStatsTest.java b/gax-java/gax/src/test/java/com/google/api/gax/batching/BatcherStatsTest.java index 1a95e4d3cb..1d10917aeb 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/batching/BatcherStatsTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/batching/BatcherStatsTest.java @@ -55,7 +55,10 @@ public void testRequestFailuresOnly() { batcherStats.recordBatchFailure( ApiExceptionFactory.createException( - new RuntimeException(), FakeStatusCode.of(StatusCode.Code.INVALID_ARGUMENT), false)); + "fake api error", + new RuntimeException(), + FakeStatusCode.of(StatusCode.Code.INVALID_ARGUMENT), + false)); batcherStats.recordBatchFailure(new RuntimeException("Request failed")); @@ -65,6 +68,10 @@ public void testRequestFailuresOnly() { assertThat(exception).hasMessageThat().contains("1 RuntimeException"); assertThat(exception).hasMessageThat().contains("1 ApiException(1 INVALID_ARGUMENT)"); assertThat(exception).hasMessageThat().contains("and 0 partial failures."); + assertThat(exception) + .hasMessageThat() + .contains( + "com.google.api.gax.rpc.InvalidArgumentException: fake api error, java.lang.RuntimeException: Request failed."); } @Test @@ -79,7 +86,10 @@ public void testEntryFailureOnly() { SettableApiFuture batchTwoResult = SettableApiFuture.create(); batchTwoResult.setException( ApiExceptionFactory.createException( - new RuntimeException(), FakeStatusCode.of(StatusCode.Code.UNAVAILABLE), false)); + "fake entry error", + new RuntimeException(), + FakeStatusCode.of(StatusCode.Code.UNAVAILABLE), + false)); batcherStats.recordBatchElementsCompletion( ImmutableList.of(BatchEntry.create(2, batchTwoResult))); @@ -89,6 +99,10 @@ public void testEntryFailureOnly() { .contains("The 2 partial failures contained 2 entries that failed with:"); assertThat(ex).hasMessageThat().contains("1 ApiException(1 UNAVAILABLE)"); assertThat(ex).hasMessageThat().contains("1 IllegalStateException"); + assertThat(ex) + .hasMessageThat() + .contains( + "Sample of entry errors: java.lang.IllegalStateException: local element failure, com.google.api.gax.rpc.UnavailableException: fake entry error."); } @Test @@ -110,6 +124,8 @@ public void testRequestAndEntryFailures() { .contains( "Batching finished with 1 batches failed to apply due to: 1 RuntimeException and 1 " + "partial failures. The 1 partial failures contained 1 entries that failed with:" - + " 1 ApiException(1 ALREADY_EXISTS)."); + + " 1 ApiException(1 ALREADY_EXISTS)." + + " Sample of RPC errors: java.lang.RuntimeException: Batch failure." + + " Sample of entry errors: com.google.api.gax.rpc.AlreadyExistsException: java.lang.RuntimeException."); } }