Monday, December 9, 2013

Handle Google AdWords API Errors or Exceptions

Both Google AdWords API and Bing Ads API defined a bunch of errors, most of these errors are not recoverable, you just need to check the error reason, fix your input data accordingly and send new operation request again. But some of the errors suggest to retry the same request in 30 seconds, for instance, CONCURRENT_MODIFICATION and RATE_EXCEEDED. Things become complicated when you enabled the partial failure feature due to you might get errors for partial change items or exceptions for the entire operation request. Here is a sample I am trying to centralize the handling of all possible errors or exceptions, so you don't have to duplicate the code here and there.

Let's say you have a method to add keywords as below:

 @Override
 public boolean addKeywords(List<BiddableKeyword> keywords) {
  logger.debug("Adding {} keywords...", keywords.size());
  List<AdGroupCriterionOperation> operations = new ArrayList<AdGroupCriterionOperation>();
  for (BiddableKeyword keyword : keywords) {
   // create keyword
   com.google.api.ads.adwords.axis.v201309.cm.Keyword newKeyword = new com.google.api.ads.adwords.axis.v201309.cm.Keyword();
   newKeyword.setText(keyword.getKeywordText());
   newKeyword.setMatchType(KeywordMatchType.fromValue(keyword.getApiMatchTypeValue()));
   // create ad group criterion
   BiddableAdGroupCriterion adGroupCriterion = new BiddableAdGroupCriterion();
   adGroupCriterion.setAdGroupId(keyword.getAdGroup().getApiId());
   adGroupCriterion.setCriterion(newKeyword);
   // update keyword status
   if (keyword.getUserStatus() != null) {
    UserStatus userStatus = UserStatus.fromValue(keyword.getUserStatusValue());
    adGroupCriterion.setUserStatus(userStatus);
   }
   // update destination url
   if (keyword.getDestinationUrl() != null) {
    adGroupCriterion.setDestinationUrl(keyword.getDestinationUrl());
   }
   // update bid amount/max cpc
   adGroupCriterion.setBiddingStrategyConfiguration(getBiddingStrategyConfiguration(keyword));
   // create operation
   AdGroupCriterionOperation operation = new AdGroupCriterionOperation();
   operation.setOperand(adGroupCriterion);
   operation.setOperator(Operator.ADD);
   // add operation
   operations.add(operation);
  }
  // submit changes now!
  int triedTimes = 0;
  while (triedTimes < Constants.API_CALL_TRY_TIMES) { // maximum 3 times
   try {
    AdGroupCriterionServiceInterface service = getAdWordsService(AdGroupCriterionServiceInterface.class);
    AdGroupCriterionReturnValue response = service.mutate(operations.toArray(new AdGroupCriterionOperation[0]));
    populateApiErrors(keywords, response.getPartialFailureErrors());
    assignReturnedCriterionIds(keywords, response);
    return response != null && updateAdParams(keywords);
   } catch (Exception exception) {
    triedTimes++;
    if (handleApiCallException(keywords, exception, triedTimes))
     break;
   }
  }
  return false;
 }

The maximum triedTimes is 3, populateApiErrors and handleApiCallException are the centralized methods to handle errors from both partial failures or thrown exceptions, the triedTimes to be passed into handleApiCallException which decides whether to abort or retry. Please check out following details:
 protected void populateApiErrors(List<? extends ApiEntity> apiEntities, com.google.api.ads.adwords.axis.v201309.cm.ApiError[] apiErrors) {
  if (apiErrors != null) {
   for (com.google.api.ads.adwords.axis.v201309.cm.ApiError apiError : apiErrors) {
    Matcher matcher = operationIndexPattern.matcher(apiError.getFieldPath());
    if (matcher.matches()) {
     int operationIndex = Integer.parseInt(matcher.group(1));
     if (apiEntities.size() > operationIndex) { // fix a potential issue!!!
      ApiEntity apiEntity = apiEntities.get(operationIndex);
      apiEntity.getApiError(true).setItemIndex(operationIndex);
      apiEntity.getApiError(true).setErrorType(apiError.getApiErrorType());
      apiEntity.getApiError(true).appendErrorString(apiError.getErrorString());
     }
    } else {
     for (ApiEntity apiEntity : apiEntities) {
      apiEntity.getApiError(true).setItemIndex(ApiError.ALL_FAILED_INDEX);
      apiEntity.getApiError(true).setErrorType(apiError.getApiErrorType());
      apiEntity.getApiError(true).appendErrorString(apiError.getErrorString());
     }
    }
   }
  }
 }

 protected boolean handleApiCallException(List<? extends ApiEntity> apiEntities, Exception exception, int triedTimes) {
  logger.debug("Got {} exception, tried {} times.", exception.getClass().getSimpleName(), triedTimes);
  if (exception instanceof ApiException) {
   ApiException apiException = (ApiException) exception;
   // Try everything possible to recover from the error
   for (com.google.api.ads.adwords.axis.v201309.cm.ApiError apiError : apiException.getErrors()) {
    if (apiError instanceof AuthenticationError)
     handleAuthenticationError((AuthenticationError) apiError);
    else if (apiError instanceof RateExceededError)
     handleRateExceededError((RateExceededError) apiError);
    else if (apiError instanceof DatabaseError)
     handleDatabaseError((DatabaseError) apiError);
    else if (apiError instanceof CustomerSyncError)
     triedTimes = handleCustomerSyncError((CustomerSyncError) apiError);
    else if (apiError instanceof InternalApiError)
     triedTimes = Constants.API_CALL_TRY_TIMES;
   }
   // Still failed, need to populate the errors
   if (triedTimes == Constants.API_CALL_TRY_TIMES) {
    populateApiErrors(apiEntities, apiException.getErrors());
    return true; // no need to try again!
   }
  } else if (exception instanceof NullPointerException) {
   populateDefaultException(apiEntities, exception);
   return true; // no need to try again!
  } else {
   if (triedTimes == Constants.API_CALL_TRY_TIMES)
    populateDefaultException(apiEntities, exception);
  }
  return false;
 }

 protected void handleAuthenticationError(AuthenticationError apiError) {
  if (apiError.getReason() == AuthenticationErrorReason.GOOGLE_ACCOUNT_COOKIE_INVALID) {
   try {
    adWordsSession.getOAuth2Credential().refreshToken();
   } catch (Exception e) {
    // do nothing
   }
  } else {
   logger.error("Service call failed for authentication reason: " + apiError.getReason());
  }
 }

 protected void handleRateExceededError(RateExceededError error) {
  logger.warn("A api call failed due to rate exceeded, will retry after {} seconds", Constants.API_CALL_WAIT_INTERVAL / 1000);
  waitForNextAPICall();
 }

 protected void handleDatabaseError(DatabaseError error) {
  if (error.getReason() == DatabaseErrorReason.CONCURRENT_MODIFICATION) {
   logger.warn("A api call failed due to concurrent modification, will retry after {} seconds", Constants.API_CALL_WAIT_INTERVAL / 1000);
   waitForNextAPICall();
  }
 }

 protected int handleCustomerSyncError(CustomerSyncError error) {
  return Constants.API_CALL_TRY_TIMES;
 }