All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gobblin.util.limiter.RedirectAwareRestClientRequestSender Maven / Gradle / Ivy

There is a newer version: 0.11.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package gobblin.util.limiter;

import java.net.ConnectException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Random;

import com.google.common.annotations.VisibleForTesting;
import com.linkedin.common.callback.Callback;
import com.linkedin.r2.RemoteInvocationException;
import com.linkedin.restli.client.Response;
import com.linkedin.restli.client.RestClient;
import com.linkedin.restli.client.RestLiResponseException;
import com.linkedin.restli.common.HttpStatus;

import gobblin.broker.ResourceInstance;
import gobblin.broker.iface.ConfigView;
import gobblin.broker.iface.NotConfiguredException;
import gobblin.broker.iface.ScopeType;
import gobblin.broker.iface.ScopedConfigView;
import gobblin.broker.iface.SharedResourceFactory;
import gobblin.broker.iface.SharedResourceFactoryResponse;
import gobblin.broker.iface.SharedResourcesBroker;
import gobblin.restli.SharedRestClientFactory;
import gobblin.restli.SharedRestClientKey;
import gobblin.restli.UriRestClientKey;
import gobblin.restli.throttling.PermitAllocation;
import gobblin.restli.throttling.PermitRequest;
import gobblin.util.ExponentialBackoff;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;


/**
 * A {@link RequestSender} that handles redirects and unreachable uris transparently.
 */
@Slf4j
public class RedirectAwareRestClientRequestSender extends RestClientRequestSender {

  /**
   * A {@link SharedResourceFactory} that creates {@link RedirectAwareRestClientRequestSender}s.
   * @param 
   */
  public static class Factory> implements SharedResourceFactory {
    @Override
    public String getName() {
      return SharedRestClientFactory.FACTORY_NAME;
    }

    @Override
    public SharedResourceFactoryResponse createResource(
        SharedResourcesBroker broker, ScopedConfigView config)
        throws NotConfiguredException {
      try {
        List connectionPrefixes = SharedRestClientFactory.parseConnectionPrefixes(config.getConfig(), config.getKey());

        return new ResourceInstance<>(
            new RedirectAwareRestClientRequestSender(broker, connectionPrefixes));
      } catch (URISyntaxException use) {
        throw new RuntimeException(use);
      }
    }

    @Override
    public S getAutoScope(SharedResourcesBroker broker, ConfigView config) {
      return broker.selfScope().getType().rootScope();
    }
  }

  private final SharedResourcesBroker broker;
  private final List connectionPrefixes;

  private volatile int lastPrefixAttempted = -1;
  private volatile RestClient restClient;
  @Getter
  private volatile String currentServerPrefix;

  /**
   * @param broker {@link SharedResourcesBroker} used to create {@link RestClient}s.
   * @param connectionPrefixes List of uri prefixes of available servers.
   * @throws NotConfiguredException
   */
  public RedirectAwareRestClientRequestSender(SharedResourcesBroker broker, List connectionPrefixes)
      throws NotConfiguredException {
    this.broker = broker;
    this.connectionPrefixes = connectionPrefixes;
    updateRestClient(getNextConnectionPrefix(), "service start");
  }

  private String getNextConnectionPrefix() {
    if (this.lastPrefixAttempted < 0) {
      this.lastPrefixAttempted = new Random().nextInt(this.connectionPrefixes.size());
    }
    this.lastPrefixAttempted = (this.lastPrefixAttempted + 1) % this.connectionPrefixes.size();
    log.info("Round robin: " + this.lastPrefixAttempted);
    return this.connectionPrefixes.get(this.lastPrefixAttempted);
  }

  @Override
  public void sendRequest(PermitRequest request, Callback> callback) {
    log.info("Sending request to " + getCurrentServerPrefix());
    super.sendRequest(request, callback);
  }

  @Override
  protected RestClient getRestClient() {
    return this.restClient;
  }

  @Override
  protected Callback> decorateCallback(PermitRequest request,
      Callback> callback) {
    if (callback instanceof CallbackDecorator) {
      return callback;
    }
    return new CallbackDecorator(request, callback);
  }

  @VisibleForTesting
  void updateRestClient(String uri, String reason) throws NotConfiguredException {
    log.info(String.format("Switching to server prefix %s due to: %s", uri, reason));
    this.currentServerPrefix = uri;
    this.restClient = (RestClient) this.broker.getSharedResource(new SharedRestClientFactory(),
          new UriRestClientKey(RestliLimiterFactory.RESTLI_SERVICE_NAME, uri));
  }

  /**
   * A {@link Callback} decorator that intercepts certain errors (301 redirects and {@link ConnectException}s) and
   * retries transparently.
   */
  @RequiredArgsConstructor
  private class CallbackDecorator implements Callback> {
    private final PermitRequest originalRequest;
    private final Callback> underlying;
    private final ExponentialBackoff exponentialBackoff = ExponentialBackoff.builder().maxDelay(10000L).build();
    private int redirects = 0;
    private int retries = 0;

    @Override
    public void onError(Throwable error) {
      try {
        if (error instanceof RestLiResponseException &&
            ((RestLiResponseException) error).getStatus() == HttpStatus.S_301_MOVED_PERMANENTLY.getCode()) {
          this.redirects++;
          if (this.redirects >= 5) {
            this.underlying.onError(new NonRetriableException("Too many redirects."));
          }
          RestLiResponseException responseExc = (RestLiResponseException) error;
          String newUri = (String) responseExc.getErrorDetails().get("Location");
          RedirectAwareRestClientRequestSender.this.updateRestClient(
              SharedRestClientFactory.resolveUriPrefix(new URI(newUri)), "301 redirect");
          this.exponentialBackoff.awaitNextRetry();
          sendRequest(this.originalRequest, this);
        } else if (error instanceof RemoteInvocationException
            && error.getCause() instanceof ConnectException) {
          this.retries++;
          if (this.retries > RedirectAwareRestClientRequestSender.this.connectionPrefixes.size()) {
            this.underlying.onError(new NonRetriableException("Failed to connect to all available connection prefixes."));
          }
          log.info("Retries " + this.retries + " this " + hashCode());
          updateRestClient(getNextConnectionPrefix(), "Failed to communicate with " + getCurrentServerPrefix());
          this.exponentialBackoff.awaitNextRetry();
          sendRequest(this.originalRequest, this);
        } else {
          this.underlying.onError(error);
        }
      } catch (Throwable t) {
        this.underlying.onError(t);
      }
    }

    @Override
    public void onSuccess(Response result) {
      this.underlying.onSuccess(result);
    }
  }
}