Skip to content

Commit d13a199

Browse files
authored
Improve partial error handling. (#488)
1 parent b77b549 commit d13a199

9 files changed

Lines changed: 148 additions & 24 deletions

File tree

google-ads-examples/src/main/java/com/google/ads/googleads/examples/errorhandling/HandlePartialFailure.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.ads.googleads.lib.GoogleAdsClient;
2323
import com.google.ads.googleads.v8.errors.GoogleAdsError;
2424
import com.google.ads.googleads.v8.errors.GoogleAdsException;
25+
import com.google.ads.googleads.v8.errors.GoogleAdsFailure;
2526
import com.google.ads.googleads.v8.resources.AdGroup;
2627
import com.google.ads.googleads.v8.services.AdGroupOperation;
2728
import com.google.ads.googleads.v8.services.AdGroupServiceClient;
@@ -30,7 +31,6 @@
3031
import com.google.ads.googleads.v8.services.MutateAdGroupsResponse;
3132
import com.google.ads.googleads.v8.utils.ErrorUtils;
3233
import com.google.ads.googleads.v8.utils.ResourceNames;
33-
import com.google.protobuf.InvalidProtocolBufferException;
3434
import java.io.FileNotFoundException;
3535
import java.io.IOException;
3636
import java.util.Arrays;
@@ -57,7 +57,7 @@ private static class HandlePartialFailureParams extends CodeSampleParams {
5757
private Long campaignId;
5858
}
5959

60-
public static void main(String[] args) throws InvalidProtocolBufferException {
60+
public static void main(String[] args) {
6161
HandlePartialFailureParams params = new HandlePartialFailureParams();
6262
if (!params.parseArguments(args)) {
6363
// Either pass the required parameters for this example on the command line, or insert them
@@ -96,8 +96,7 @@ public static void main(String[] args) throws InvalidProtocolBufferException {
9696
}
9797

9898
/** Runs the example. */
99-
public void runExample(GoogleAdsClient googleAdsClient, long customerId, long campaignId)
100-
throws InvalidProtocolBufferException {
99+
public void runExample(GoogleAdsClient googleAdsClient, long customerId, long campaignId) {
101100
MutateAdGroupsResponse response = createAdGroups(googleAdsClient, customerId, campaignId);
102101

103102
// Checks for existence of any partial failures in the response.
@@ -165,15 +164,18 @@ private boolean checkIfPartialFailureErrorExists(MutateAdGroupsResponse response
165164

166165
/** Displays the result from the mutate operation. */
167166
// [START handle_partial_failure_2]
168-
private void printResults(MutateAdGroupsResponse response) throws InvalidProtocolBufferException {
167+
private void printResults(MutateAdGroupsResponse response) {
169168
int operationIndex = 0;
170169
for (MutateAdGroupResult result : response.getResultsList()) {
171170
if (ErrorUtils.getInstance().isPartialFailureResult(result)) {
172171
// May throw on this line. Most likely this means the wrong version of the ErrorUtils
173172
// class has been used.
173+
GoogleAdsFailure googleAdsFailure = ErrorUtils.getInstance()
174+
.getGoogleAdsFailure(response.getPartialFailureError());
175+
174176
for (GoogleAdsError error :
175177
ErrorUtils.getInstance()
176-
.getGoogleAdsErrors(operationIndex, response.getPartialFailureError())) {
178+
.getGoogleAdsErrors(operationIndex, googleAdsFailure)) {
177179
System.out.printf("Operation %d failed with error: %s%n", operationIndex, error);
178180
}
179181
} else {

google-ads-examples/src/main/java/com/google/ads/googleads/examples/remarketing/AddCustomerMatchUserList.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.ads.googleads.v8.enums.OfflineUserDataJobTypeEnum.OfflineUserDataJobType;
3131
import com.google.ads.googleads.v8.errors.GoogleAdsError;
3232
import com.google.ads.googleads.v8.errors.GoogleAdsException;
33+
import com.google.ads.googleads.v8.errors.GoogleAdsFailure;
3334
import com.google.ads.googleads.v8.resources.OfflineUserDataJob;
3435
import com.google.ads.googleads.v8.resources.UserList;
3536
import com.google.ads.googleads.v8.services.AddOfflineUserDataJobOperationsRequest;
@@ -44,6 +45,7 @@
4445
import com.google.ads.googleads.v8.services.SearchGoogleAdsStreamResponse;
4546
import com.google.ads.googleads.v8.services.UserListOperation;
4647
import com.google.ads.googleads.v8.services.UserListServiceClient;
48+
import com.google.ads.googleads.v8.utils.ErrorUtils;
4749
import com.google.api.gax.rpc.ServerStream;
4850
import com.google.common.collect.ImmutableList;
4951
import java.io.FileNotFoundException;
@@ -219,11 +221,13 @@ private void addUsersToCustomerMatchUserList(
219221
// NOTE: The details of each partial failure error are not printed here, you can refer to
220222
// the example HandlePartialFailure.java to learn more.
221223
if (response.hasPartialFailureError()) {
224+
GoogleAdsFailure googleAdsFailure =
225+
ErrorUtils.getInstance().getGoogleAdsFailure(response.getPartialFailureError());
222226
System.out.printf(
223227
"Encountered %d partial failure errors while adding %d operations to the offline user "
224228
+ "data job: '%s'. Only the successfully added operations will be executed when "
225229
+ "the job runs.%n",
226-
response.getPartialFailureError().getDetailsCount(),
230+
googleAdsFailure.getErrorsCount(),
227231
userDataJobOperations.size(),
228232
response.getPartialFailureError().getMessage());
229233
} else {

google-ads-examples/src/main/java/com/google/ads/googleads/examples/remarketing/UploadCallConversion.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
import com.google.ads.googleads.lib.GoogleAdsClient;
2121
import com.google.ads.googleads.v8.errors.GoogleAdsError;
2222
import com.google.ads.googleads.v8.errors.GoogleAdsException;
23+
import com.google.ads.googleads.v8.errors.GoogleAdsFailure;
2324
import com.google.ads.googleads.v8.services.CallConversion;
2425
import com.google.ads.googleads.v8.services.CallConversionResult;
2526
import com.google.ads.googleads.v8.services.ConversionUploadServiceClient;
2627
import com.google.ads.googleads.v8.services.CustomVariable;
2728
import com.google.ads.googleads.v8.services.UploadCallConversionsRequest;
2829
import com.google.ads.googleads.v8.services.UploadCallConversionsResponse;
30+
import com.google.ads.googleads.v8.utils.ErrorUtils;
2931
import com.google.ads.googleads.v8.utils.ResourceNames;
3032
import java.io.FileNotFoundException;
3133
import java.io.IOException;
@@ -185,6 +187,11 @@ private void runExample(
185187

186188
// Prints any partial failure errors returned.
187189
if (response.hasPartialFailureError()) {
190+
GoogleAdsFailure googleAdsFailure =
191+
ErrorUtils.getInstance().getGoogleAdsFailure(response.getPartialFailureError());
192+
googleAdsFailure
193+
.getErrorsList()
194+
.forEach(e -> System.out.println("Partial failure occurred: " + e.getMessage()));
188195
throw new RuntimeException(
189196
"Partial failure occurred " + response.getPartialFailureError().getMessage());
190197
}

google-ads-examples/src/main/java/com/google/ads/googleads/examples/remarketing/UploadConversionAdjustment.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
import com.google.ads.googleads.v8.enums.ConversionAdjustmentTypeEnum.ConversionAdjustmentType;
2222
import com.google.ads.googleads.v8.errors.GoogleAdsError;
2323
import com.google.ads.googleads.v8.errors.GoogleAdsException;
24+
import com.google.ads.googleads.v8.errors.GoogleAdsFailure;
2425
import com.google.ads.googleads.v8.services.ConversionAdjustment;
2526
import com.google.ads.googleads.v8.services.ConversionAdjustmentResult;
2627
import com.google.ads.googleads.v8.services.ConversionAdjustmentUploadServiceClient;
2728
import com.google.ads.googleads.v8.services.GclidDateTimePair;
2829
import com.google.ads.googleads.v8.services.RestatementValue;
2930
import com.google.ads.googleads.v8.services.UploadConversionAdjustmentsRequest;
3031
import com.google.ads.googleads.v8.services.UploadConversionAdjustmentsResponse;
32+
import com.google.ads.googleads.v8.utils.ErrorUtils;
3133
import com.google.ads.googleads.v8.utils.ResourceNames;
3234
import java.io.FileNotFoundException;
3335
import java.io.IOException;
@@ -203,8 +205,11 @@ private void runExample(
203205

204206
// Prints any partial errors returned.
205207
if (response.hasPartialFailureError()) {
206-
System.out.printf(
207-
"Partial error encountered: '%s'.%n", response.getPartialFailureError().getMessage());
208+
GoogleAdsFailure googleAdsFailure =
209+
ErrorUtils.getInstance().getGoogleAdsFailure(response.getPartialFailureError());
210+
googleAdsFailure
211+
.getErrorsList()
212+
.forEach(e -> System.out.println("Partial failure occurred: " + e.getMessage()));
208213
} else {
209214
// Prints the result.
210215
ConversionAdjustmentResult result = response.getResults(0);

google-ads-examples/src/main/java/com/google/ads/googleads/examples/remarketing/UploadOfflineConversion.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
import com.google.ads.googleads.lib.GoogleAdsClient;
2121
import com.google.ads.googleads.v8.errors.GoogleAdsError;
2222
import com.google.ads.googleads.v8.errors.GoogleAdsException;
23+
import com.google.ads.googleads.v8.errors.GoogleAdsFailure;
2324
import com.google.ads.googleads.v8.services.ClickConversion;
2425
import com.google.ads.googleads.v8.services.ClickConversionResult;
2526
import com.google.ads.googleads.v8.services.ConversionUploadServiceClient;
2627
import com.google.ads.googleads.v8.services.CustomVariable;
2728
import com.google.ads.googleads.v8.services.UploadClickConversionsRequest;
2829
import com.google.ads.googleads.v8.services.UploadClickConversionsResponse;
30+
import com.google.ads.googleads.v8.utils.ErrorUtils;
2931
import com.google.ads.googleads.v8.utils.ResourceNames;
3032
import java.io.FileNotFoundException;
3133
import java.io.IOException;
@@ -198,8 +200,11 @@ private void runExample(
198200

199201
// Prints any partial errors returned.
200202
if (response.hasPartialFailureError()) {
201-
System.out.printf(
202-
"Partial error encountered: '%s'.%n", response.getPartialFailureError().getMessage());
203+
GoogleAdsFailure googleAdsFailure =
204+
ErrorUtils.getInstance().getGoogleAdsFailure(response.getPartialFailureError());
205+
googleAdsFailure
206+
.getErrorsList()
207+
.forEach(e -> System.out.println("Partial failure occurred: " + e.getMessage()));
203208
}
204209

205210
// Prints the result.

google-ads-examples/src/main/java/com/google/ads/googleads/examples/remarketing/UploadStoreSalesTransactions.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.ads.googleads.v8.enums.OfflineUserDataJobTypeEnum.OfflineUserDataJobType;
2929
import com.google.ads.googleads.v8.errors.GoogleAdsError;
3030
import com.google.ads.googleads.v8.errors.GoogleAdsException;
31+
import com.google.ads.googleads.v8.errors.GoogleAdsFailure;
3132
import com.google.ads.googleads.v8.resources.OfflineUserDataJob;
3233
import com.google.ads.googleads.v8.services.AddOfflineUserDataJobOperationsRequest;
3334
import com.google.ads.googleads.v8.services.AddOfflineUserDataJobOperationsResponse;
@@ -36,6 +37,7 @@
3637
import com.google.ads.googleads.v8.services.GoogleAdsServiceClient;
3738
import com.google.ads.googleads.v8.services.OfflineUserDataJobOperation;
3839
import com.google.ads.googleads.v8.services.OfflineUserDataJobServiceClient;
40+
import com.google.ads.googleads.v8.utils.ErrorUtils;
3941
import com.google.ads.googleads.v8.utils.ResourceNames;
4042
import com.google.common.collect.ImmutableList;
4143
import java.io.FileNotFoundException;
@@ -469,11 +471,16 @@ private void addTransactionsToOfflineUserDataJob(
469471
// NOTE: The details of each partial failure error are not printed here, you can refer to
470472
// the example HandlePartialFailure.java to learn more.
471473
if (response.hasPartialFailureError()) {
474+
GoogleAdsFailure googleAdsFailure =
475+
ErrorUtils.getInstance().getGoogleAdsFailure(response.getPartialFailureError());
476+
googleAdsFailure
477+
.getErrorsList()
478+
.forEach(e -> System.out.println("Partial failure occurred: " + e.getMessage()));
472479
System.out.printf(
473480
"Encountered %d partial failure errors while adding %d operations to the offline user "
474481
+ "data job: '%s'. Only the successfully added operations will be executed when "
475482
+ "the job runs.%n",
476-
response.getPartialFailureError().getDetailsCount(),
483+
ErrorUtils.getInstance().getFailedOperationIndices(googleAdsFailure).size(),
477484
userDataJobOperations.size(),
478485
response.getPartialFailureError().getMessage());
479486
} else {

google-ads/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ sourceSets {
4949
runtimeClasspath += sourceSets.main.output
5050
}
5151
}
52+
sourceSets.test.java.srcDir new File(buildDir, 'generated/source/proto/test')
5253

5354
protobuf {
5455
protoc {

google-ads/src/main/java/com/google/ads/googleads/lib/utils/AbstractErrorUtils.java

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.util.ArrayList;
2222
import java.util.List;
2323
import java.util.Optional;
24+
import java.util.stream.Collectors;
25+
import java.util.stream.StreamSupport;
2426

2527
/** Contains utility methods for handling partial failure of operations. */
2628
public abstract class AbstractErrorUtils<
@@ -77,9 +79,9 @@ public abstract class AbstractErrorUtils<
7779
* empty list otherwise.
7880
*
7981
* <p>This method supports <code>XXXService.mutate(request)</code> where the request contains a
80-
* list of operations named "operations". It also supports
81-
* <code>GoogleAdsService.mutateGoogleAds(request)</code>, where the request contains a list of
82-
* <code>MutateOperation</code>s named "mutate_operations".
82+
* list of operations named "operations". It also supports <code>
83+
* GoogleAdsService.mutateGoogleAds(request)</code>, where the request contains a list of <code>
84+
* MutateOperation</code>s named "mutate_operations".
8385
*
8486
* @param operationIndex the index of the operation, starting from 0.
8587
* @param partialFailureStatus a partialFailure status, with the detail list containing {@link
@@ -108,8 +110,7 @@ public List<GoogleAdsErrorT> getGoogleAdsErrors(
108110
List<GoogleAdsErrorT> result = new ArrayList();
109111
// Searches all the errors for one relating to the specified operation.
110112
for (ErrorPath<GoogleAdsErrorT> path : getErrorPaths(googleAdsFailure)) {
111-
if (("operations".equals(path.getFieldName())
112-
|| "mutate_operations".equals(path.getFieldName()))
113+
if (path.isOperationIndex()
113114
&& path.getIndex().isPresent()
114115
&& path.getIndex().get() == operationIndex) {
115116
GoogleAdsErrorT error = path.getError();
@@ -121,15 +122,49 @@ public List<GoogleAdsErrorT> getGoogleAdsErrors(
121122
return result;
122123
}
123124

125+
/** Provides a convenience method to get all failed operation indices. */
126+
public List<Long> getFailedOperationIndices(GoogleAdsFailureT googleAdsFailureT) {
127+
return StreamSupport.stream(getErrorPaths(googleAdsFailureT).spliterator(), false)
128+
.filter(ErrorPath::isOperationIndex)
129+
.filter(p -> p.getIndex().isPresent())
130+
.map(p -> (Long) p.getIndex().get())
131+
.distinct()
132+
.collect(Collectors.toList());
133+
}
134+
124135
/**
125136
* Unpacks a single {@link GoogleAdsFailureT} from an {@link Any} instance.
126137
*
127-
* @throws InvalidProtocolBufferException if {@link GoogleAdsFailureT} is not able to unpack the
128-
* protocol buffer. This is most likely due to using the wrong version of <code>ErrorUtils
129-
* </code> being used.
138+
* @throws DeserializeException if an {@link InvalidProtocolBufferException} is encountered. This
139+
* would indicate that the detail object was not-null, but the contents couldn't be
140+
* deserialized to the target type. This may indicate that the target type is incorrect, or
141+
* that the content of the Any message is incorrect.
142+
* @throws NullPointerException if detail is null.
130143
*/
131-
public GoogleAdsFailureT getGoogleAdsFailure(Any detail) throws InvalidProtocolBufferException {
132-
return detail.unpack(getGoogleAdsFailureClass());
144+
public GoogleAdsFailureT getGoogleAdsFailure(Any detail) {
145+
try {
146+
return detail.unpack(getGoogleAdsFailureClass());
147+
} catch (InvalidProtocolBufferException e) {
148+
throw new DeserializeException(e);
149+
}
150+
}
151+
152+
/**
153+
* Unpacks the GoogleAdsFailureT instance form a partial failure status object.
154+
*
155+
* <p>The status object contains a details repeated field. This contains at most 1 Any protos
156+
* which encode a GoogleAdsFailure instance.
157+
*
158+
* @param partialFailureStatus the partial failure Status object returned in the repsponse.
159+
* @return the GoogleAdsFailure instance describing the partial failures, or null if none is
160+
* found.
161+
* @throws DeserializeException if an {@link InvalidProtocolBufferException} is encountered.
162+
* @throws NullPointerException if partialFailureStatus is null.
163+
*/
164+
public GoogleAdsFailureT getGoogleAdsFailure(Status partialFailureStatus) {
165+
return partialFailureStatus.getDetailsCount() == 0
166+
? null
167+
: getGoogleAdsFailure(partialFailureStatus.getDetails(0));
133168
}
134169

135170
/** Checks if a result in a mutate response is a partial failure. */
@@ -192,8 +227,7 @@ protected static class ErrorPath<GoogleAdsErrorType extends Message> {
192227
private final String fieldName;
193228
private final Optional<Long> index;
194229

195-
public ErrorPath(
196-
GoogleAdsErrorType error, String fieldName, Optional<Long> index) {
230+
public ErrorPath(GoogleAdsErrorType error, String fieldName, Optional<Long> index) {
197231
this.error = error;
198232
this.fieldName = fieldName;
199233
this.index = index;
@@ -210,5 +244,17 @@ public String getFieldName() {
210244
public Optional<Long> getIndex() {
211245
return index;
212246
}
247+
248+
public boolean isOperationIndex() {
249+
return "operations".equals(getFieldName()) || "mutate_operations".equals(getFieldName());
250+
}
251+
}
252+
253+
/** Indicates an error occurred deserializing an API error object. */
254+
public static class DeserializeException extends RuntimeException {
255+
256+
public DeserializeException(Throwable cause) {
257+
super(cause);
258+
}
213259
}
214260
}

google-ads/src/test/java/com/google/ads/googleads/lib/utils/AbstractErrorUtilsTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,53 @@ public void getGoogleAdsErrors_duplicates_whenErrorsDiffer()
176176
assertEquals(error1, result.get(1));
177177
}
178178

179+
@Test
180+
public void getFailedOperationIndices_returnsOperation() {
181+
MockPath path0 =
182+
MockPath.newBuilder()
183+
.setIndex(Int64Value.newBuilder().setValue(123))
184+
.setFieldName(operationsFieldName)
185+
.build();
186+
MockError error0 = MockError.newBuilder().addLocation(path0).build();
187+
MockFailure failure = MockFailure.newBuilder().addErrors(error0).build();
188+
List<Long> result = impl.getFailedOperationIndices(failure);
189+
assertEquals(1, result.size());
190+
assertEquals(123L, (long) result.get(0));
191+
}
192+
193+
@Test
194+
public void getFailedOperationIndices_removesDuplicates() {
195+
MockPath path0 =
196+
MockPath.newBuilder()
197+
.setIndex(Int64Value.newBuilder().setValue(123))
198+
.setFieldName(operationsFieldName)
199+
.build();
200+
MockPath path1 =
201+
MockPath.newBuilder()
202+
.setIndex(Int64Value.newBuilder().setValue(123))
203+
.setFieldName(operationsFieldName)
204+
.build();
205+
MockError error0 = MockError.newBuilder().addLocation(path0).build();
206+
MockError error1 = MockError.newBuilder().addLocation(path1).build();
207+
MockFailure failure = MockFailure.newBuilder().addErrors(error0).addErrors(error1).build();
208+
List<Long> result = impl.getFailedOperationIndices(failure);
209+
assertEquals(1, result.size());
210+
assertEquals(123L, (long) result.get(0));
211+
}
212+
213+
@Test
214+
public void getFailedOperationIndices_ignoresNonOperationErrors() {
215+
MockPath path0 =
216+
MockPath.newBuilder()
217+
.setIndex(Int64Value.newBuilder().setValue(123))
218+
.setFieldName("someotherfield")
219+
.build();
220+
MockError error0 = MockError.newBuilder().addLocation(path0).build();
221+
MockFailure failure = MockFailure.newBuilder().addErrors(error0).build();
222+
List<Long> result = impl.getFailedOperationIndices(failure);
223+
assertEquals(0, result.size());
224+
}
225+
179226
// We do want a dummy here for the version specific code, rather than a mock, so we can test the
180227
// base class methods.
181228
private static class TestImpl extends AbstractErrorUtils<MockFailure, MockError, MockPath> {

0 commit comments

Comments
 (0)