Code review comment: A whole lot of trouble and non-compliance fees, see https://developers.google.com/google-ads/api/docs/rate-sheet. I've given this a bit more thought and I believe that the policy configuration needs to be owned entirely by the user without giving the illusion of safe defaults, since there simply are none that we can safely recommend due to the complexity of the APIs quotas and limits and their global applicability to developer tokens or customer IDs.  I will rename `DefaultRateLimitPolicy` to `FixedRateLimitPolicy`, mark it public, and leave `rateLimitPolicyFactory` set to null on construction. I'll mark FixedRateLimitPolicy as clearly not intended for advanced users who may exceed their global quota across multiple running applications.  Advanced users should implement their own shared token bucket service (e.g. using https://github.com/mailgun/gubernator) as per the advice provided at https://developers.google.com/google-ads/api/docs/best-practices/rate-limits#throttle.  Moving DatastoreIO's AdaptiveThrottler to a util package would offer a helpful building block for users to apply an additional layer of client side protection against exceeding limits. I'm planning a follow up PR to do that.  Upon reviewing DatastoreIO and FirestoreIO it occurred to me that neither of them apply a pipeline global rate limit across all workers, but divide the desired pipeline global QPS by maxNumWorkers, with their default being 1 QPS local to the DoFn.

Code under review:
public void processElement(ProcessContext c) throws IOException, InterruptedException {
        GoogleAdsClient googleAdsClient = checkStateNotNull(this.googleAdsClient);
        GoogleAdsServiceClient googleAdsServiceClient =
            checkStateNotNull(this.googleAdsServiceClient);
        RateLimitPolicy rateLimitPolicy = checkStateNotNull(this.rateLimitPolicy);

        BackOff backoff = BACKOFF.backoff();
        BackOff nextBackoff = backoff;
        GoogleAdsException lastException = null;

        SearchGoogleAdsStreamRequest request = c.element();
        String developerToken = googleAdsClient.getDeveloperToken();
        String customerId = request.getCustomerId();

        do {
          rateLimitPolicy.onBeforeRequest(developerToken, customerId, request);

          try {
            for (SearchGoogleAdsStreamResponse response :
                googleAdsServiceClient.searchStreamCallable().call(request)) {
              for (GoogleAdsRow row : response.getResultsList()) {
                c.output(row);
              }
            }
            rateLimitPolicy.onSuccess(developerToken, customerId, request);
            return;
          } catch (GoogleAdsException e) {
            GoogleAdsError retryableError =
                findFirstRetryableError(e.getGoogleAdsFailure())
                    .orElseThrow(() -> new IOException(e));

            rateLimitPolicy.onError(developerToken, customerId, request, retryableError);

            
            
            
            if (retryableError.getDetails().getQuotaErrorDetails().hasRetryDelay()) {
              nextBackoff =
                  new BackOff() {
                    @Override
                    public void reset() {}

                    @Override
                    public long nextBackOffMillis() {
                      return Durations.toMillis(
                          retryableError.getDetails().getQuotaErrorDetails().getRetryDelay());
                    }
                  };
            } else {
              nextBackoff = backoff;
            }
          }
        } while (BackOffUtils.next(sleeper, nextBackoff));

        throw new IOException(
            String.format(
                "Unable to get Google Ads response after retrying %d times using query (%s)",
                MAX_RETRIES, request.getQuery()),
            lastException);
      }

Line commented on: BackOff backoff = BACKOFF.backoff();

Revised code:
public void processElement(ProcessContext c) throws IOException, InterruptedException {
        final GoogleAdsClient googleAdsClient = this.googleAdsClient;
        final GoogleAdsServiceClient googleAdsServiceClient = this.googleAdsServiceClient;
        final RateLimitPolicy rateLimitPolicy = this.rateLimitPolicy;

        BackOff backoff = BACKOFF.backoff();
        BackOff nextBackoff = backoff;
        GoogleAdsException lastException = null;

        SearchGoogleAdsStreamRequest request = c.element();
        String developerToken = googleAdsClient.getDeveloperToken();
        String customerId = request.getCustomerId();

        do {
          rateLimitPolicy.onBeforeRequest(developerToken, customerId, request);

          try {
            for (SearchGoogleAdsStreamResponse response :
                googleAdsServiceClient.searchStreamCallable().call(request)) {
              for (GoogleAdsRow row : response.getResultsList()) {
                c.output(row);
              }
            }
            rateLimitPolicy.onSuccess(developerToken, customerId, request);
            return;
          } catch (GoogleAdsException e) {
            GoogleAdsError retryableError =
                findFirstRetryableError(e.getGoogleAdsFailure())
                    .orElseThrow(() -> new IOException(e));

            rateLimitPolicy.onError(developerToken, customerId, request, retryableError);

            
            
            
            if (retryableError.getDetails().getQuotaErrorDetails().hasRetryDelay()) {
              nextBackoff =
                  new BackOff() {
                    @Override
                    public void reset() {}

                    @Override
                    public long nextBackOffMillis() {
                      return Durations.toMillis(
                          retryableError.getDetails().getQuotaErrorDetails().getRetryDelay());
                    }
                  };
            } else {
              nextBackoff = backoff;
            }
          }
        } while (BackOffUtils.next(sleeper, nextBackoff));

        throw new IOException(
            String.format(
                "Unable to get Google Ads response after retrying %d times using query (%s)",
                MAX_RETRIES, request.getQuery()),
            lastException);
      }