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

io.grpc.internal.ServiceConfigInterceptor Maven / Gradle / Ivy

There is a newer version: 1.66.0
Show newest version
/*
 * Copyright 2018 The gRPC Authors
 *
 * Licensed 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 io.grpc.internal;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Verify.verify;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.Deadline;
import io.grpc.MethodDescriptor;
import io.grpc.Status.Code;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

/**
 * Modifies RPCs in conformance with a Service Config.
 */
final class ServiceConfigInterceptor implements ClientInterceptor {

  private static final Logger logger = Logger.getLogger(ServiceConfigInterceptor.class.getName());

  // Map from method name to MethodInfo
  @VisibleForTesting
  final AtomicReference> serviceMethodMap
      = new AtomicReference<>();
  @VisibleForTesting
  final AtomicReference> serviceMap
      = new AtomicReference<>();

  private final boolean retryEnabled;
  private final int maxRetryAttemptsLimit;
  private final int maxHedgedAttemptsLimit;

  // Setting this to true and observing this equal to true are run in different threads.
  private volatile boolean nameResolveComplete;

  ServiceConfigInterceptor(
      boolean retryEnabled, int maxRetryAttemptsLimit, int maxHedgedAttemptsLimit) {
    this.retryEnabled = retryEnabled;
    this.maxRetryAttemptsLimit = maxRetryAttemptsLimit;
    this.maxHedgedAttemptsLimit = maxHedgedAttemptsLimit;
  }

  void handleUpdate(@Nonnull Map serviceConfig) {
    Map newServiceMethodConfigs = new HashMap<>();
    Map newServiceConfigs = new HashMap<>();

    // Try and do as much validation here before we swap out the existing configuration.  In case
    // the input is invalid, we don't want to lose the existing configuration.

    List> methodConfigs =
        ServiceConfigUtil.getMethodConfigFromServiceConfig(serviceConfig);
    if (methodConfigs == null) {
      logger.log(Level.FINE, "No method configs found, skipping");
      nameResolveComplete = true;
      return;
    }

    for (Map methodConfig : methodConfigs) {
      MethodInfo info = new MethodInfo(
          methodConfig, retryEnabled, maxRetryAttemptsLimit, maxHedgedAttemptsLimit);

      List> nameList =
          ServiceConfigUtil.getNameListFromMethodConfig(methodConfig);

      checkArgument(
          nameList != null && !nameList.isEmpty(), "no names in method config %s", methodConfig);
      for (Map name : nameList) {
        String serviceName = ServiceConfigUtil.getServiceFromName(name);
        checkArgument(!Strings.isNullOrEmpty(serviceName), "missing service name");
        String methodName = ServiceConfigUtil.getMethodFromName(name);
        if (Strings.isNullOrEmpty(methodName)) {
          // Service scoped config
          checkArgument(
              !newServiceConfigs.containsKey(serviceName), "Duplicate service %s", serviceName);
          newServiceConfigs.put(serviceName, info);
        } else {
          // Method scoped config
          String fullMethodName = MethodDescriptor.generateFullMethodName(serviceName, methodName);
          checkArgument(
              !newServiceMethodConfigs.containsKey(fullMethodName),
              "Duplicate method name %s",
              fullMethodName);
          newServiceMethodConfigs.put(fullMethodName, info);
        }
      }
    }

    // Okay, service config is good, swap it.
    serviceMethodMap.set(Collections.unmodifiableMap(newServiceMethodConfigs));
    serviceMap.set(Collections.unmodifiableMap(newServiceConfigs));
    nameResolveComplete = true;
  }

  /**
   * Equivalent of MethodConfig from a ServiceConfig with restrictions from Channel setting.
   */
  static final class MethodInfo {
    final Long timeoutNanos;
    final Boolean waitForReady;
    final Integer maxInboundMessageSize;
    final Integer maxOutboundMessageSize;
    final RetryPolicy retryPolicy;
    final HedgingPolicy hedgingPolicy;

    /**
     * Constructor.
     *
     * @param retryEnabled when false, the argument maxRetryAttemptsLimit will have no effect.
     */
    MethodInfo(
        Map methodConfig, boolean retryEnabled, int maxRetryAttemptsLimit,
        int maxHedgedAttemptsLimit) {
      timeoutNanos = ServiceConfigUtil.getTimeoutFromMethodConfig(methodConfig);
      waitForReady = ServiceConfigUtil.getWaitForReadyFromMethodConfig(methodConfig);
      maxInboundMessageSize =
          ServiceConfigUtil.getMaxResponseMessageBytesFromMethodConfig(methodConfig);
      if (maxInboundMessageSize != null) {
        checkArgument(
            maxInboundMessageSize >= 0,
            "maxInboundMessageSize %s exceeds bounds", maxInboundMessageSize);
      }
      maxOutboundMessageSize =
          ServiceConfigUtil.getMaxRequestMessageBytesFromMethodConfig(methodConfig);
      if (maxOutboundMessageSize != null) {
        checkArgument(
            maxOutboundMessageSize >= 0,
            "maxOutboundMessageSize %s exceeds bounds", maxOutboundMessageSize);
      }

      Map retryPolicyMap =
          retryEnabled ? ServiceConfigUtil.getRetryPolicyFromMethodConfig(methodConfig) : null;
      retryPolicy = retryPolicyMap == null
          ? RetryPolicy.DEFAULT : retryPolicy(retryPolicyMap, maxRetryAttemptsLimit);

      Map hedgingPolicyMap =
          retryEnabled ? ServiceConfigUtil.getHedgingPolicyFromMethodConfig(methodConfig) : null;
      hedgingPolicy = hedgingPolicyMap == null
          ? HedgingPolicy.DEFAULT : hedgingPolicy(hedgingPolicyMap, maxHedgedAttemptsLimit);
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(
          timeoutNanos, waitForReady, maxInboundMessageSize, maxOutboundMessageSize, retryPolicy);
    }

    @Override
    public boolean equals(Object other) {
      if (!(other instanceof MethodInfo)) {
        return false;
      }
      MethodInfo that = (MethodInfo) other;
      return Objects.equal(this.timeoutNanos, that.timeoutNanos)
          && Objects.equal(this.waitForReady, that.waitForReady)
          && Objects.equal(this.maxInboundMessageSize, that.maxInboundMessageSize)
          && Objects.equal(this.maxOutboundMessageSize, that.maxOutboundMessageSize)
          && Objects.equal(this.retryPolicy, that.retryPolicy);
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("timeoutNanos", timeoutNanos)
          .add("waitForReady", waitForReady)
          .add("maxInboundMessageSize", maxInboundMessageSize)
          .add("maxOutboundMessageSize", maxOutboundMessageSize)
          .add("retryPolicy", retryPolicy)
          .toString();
    }

    private static RetryPolicy retryPolicy(Map retryPolicy, int maxAttemptsLimit) {
      int maxAttempts = checkNotNull(
          ServiceConfigUtil.getMaxAttemptsFromRetryPolicy(retryPolicy),
          "maxAttempts cannot be empty");
      checkArgument(maxAttempts >= 2, "maxAttempts must be greater than 1: %s", maxAttempts);
      maxAttempts = Math.min(maxAttempts, maxAttemptsLimit);

      long initialBackoffNanos = checkNotNull(
          ServiceConfigUtil.getInitialBackoffNanosFromRetryPolicy(retryPolicy),
          "initialBackoff cannot be empty");
      checkArgument(
          initialBackoffNanos > 0,
          "initialBackoffNanos must be greater than 0: %s",
          initialBackoffNanos);

      long maxBackoffNanos = checkNotNull(
          ServiceConfigUtil.getMaxBackoffNanosFromRetryPolicy(retryPolicy),
          "maxBackoff cannot be empty");
      checkArgument(
          maxBackoffNanos > 0, "maxBackoff must be greater than 0: %s", maxBackoffNanos);

      double backoffMultiplier = checkNotNull(
          ServiceConfigUtil.getBackoffMultiplierFromRetryPolicy(retryPolicy),
          "backoffMultiplier cannot be empty");
      checkArgument(
          backoffMultiplier > 0,
          "backoffMultiplier must be greater than 0: %s",
          backoffMultiplier);

      List rawCodes =
          ServiceConfigUtil.getRetryableStatusCodesFromRetryPolicy(retryPolicy);
      checkNotNull(rawCodes, "rawCodes must be present");
      checkArgument(!rawCodes.isEmpty(), "rawCodes can't be empty");
      EnumSet codes = EnumSet.noneOf(Code.class);
      // service config doesn't say if duplicates are allowed, so just accept them.
      for (String rawCode : rawCodes) {
        verify(!"OK".equals(rawCode), "rawCode can not be \"OK\"");
        codes.add(Code.valueOf(rawCode));
      }
      Set retryableStatusCodes = Collections.unmodifiableSet(codes);

      return new RetryPolicy(
          maxAttempts, initialBackoffNanos, maxBackoffNanos, backoffMultiplier,
          retryableStatusCodes);
    }
  }

  private static HedgingPolicy hedgingPolicy(
      Map hedgingPolicy, int maxAttemptsLimit) {
    int maxAttempts = checkNotNull(
        ServiceConfigUtil.getMaxAttemptsFromHedgingPolicy(hedgingPolicy),
        "maxAttempts cannot be empty");
    checkArgument(maxAttempts >= 2, "maxAttempts must be greater than 1: %s", maxAttempts);
    maxAttempts = Math.min(maxAttempts, maxAttemptsLimit);

    long hedgingDelayNanos = checkNotNull(
        ServiceConfigUtil.getHedgingDelayNanosFromHedgingPolicy(hedgingPolicy),
        "hedgingDelay cannot be empty");
    checkArgument(
        hedgingDelayNanos >= 0, "hedgingDelay must not be negative: %s", hedgingDelayNanos);

    List rawCodes =
        ServiceConfigUtil.getNonFatalStatusCodesFromHedgingPolicy(hedgingPolicy);
    checkNotNull(rawCodes, "rawCodes must be present");
    checkArgument(!rawCodes.isEmpty(), "rawCodes can't be empty");
    EnumSet codes = EnumSet.noneOf(Code.class);
    // service config doesn't say if duplicates are allowed, so just accept them.
    for (String rawCode : rawCodes) {
      verify(!"OK".equals(rawCode), "rawCode can not be \"OK\"");
      codes.add(Code.valueOf(rawCode));
    }
    Set nonFatalStatusCodes = Collections.unmodifiableSet(codes);

    return new HedgingPolicy(maxAttempts, hedgingDelayNanos, nonFatalStatusCodes);
  }

  static final CallOptions.Key RETRY_POLICY_KEY =
      CallOptions.Key.create("internal-retry-policy");
  static final CallOptions.Key HEDGING_POLICY_KEY =
      CallOptions.Key.create("internal-hedging-policy");

  @Override
  public  ClientCall interceptCall(
      final MethodDescriptor method, CallOptions callOptions, Channel next) {
    if (retryEnabled) {
      if (nameResolveComplete) {
        final RetryPolicy retryPolicy = getRetryPolicyFromConfig(method);
        final class ImmediateRetryPolicyProvider implements RetryPolicy.Provider {
          @Override
          public RetryPolicy get() {
            return retryPolicy;
          }
        }

        final HedgingPolicy hedgingPolicy = getHedgingPolicyFromConfig(method);
        final class ImmediateHedgingPolicyProvider implements HedgingPolicy.Provider {
          @Override
          public HedgingPolicy get() {
            return hedgingPolicy;
          }
        }

        verify(
            retryPolicy.equals(RetryPolicy.DEFAULT) || hedgingPolicy.equals(HedgingPolicy.DEFAULT),
            "Can not apply both retry and hedging policy for the method '%s'", method);

        callOptions = callOptions
            .withOption(RETRY_POLICY_KEY, new ImmediateRetryPolicyProvider())
            .withOption(HEDGING_POLICY_KEY, new ImmediateHedgingPolicyProvider());
      } else {
        final class DelayedRetryPolicyProvider implements RetryPolicy.Provider {
          /**
           * Returns RetryPolicy.DEFAULT if name resolving is not complete at the moment the method
           * is invoked, otherwise returns the RetryPolicy computed from service config.
           *
           * 

Note that this method is used no more than once for each call. */ @Override public RetryPolicy get() { if (!nameResolveComplete) { return RetryPolicy.DEFAULT; } return getRetryPolicyFromConfig(method); } } final class DelayedHedgingPolicyProvider implements HedgingPolicy.Provider { /** * Returns HedgingPolicy.DEFAULT if name resolving is not complete at the moment the * method is invoked, otherwise returns the HedgingPolicy computed from service config. * *

Note that this method is used no more than once for each call. */ @Override public HedgingPolicy get() { if (!nameResolveComplete) { return HedgingPolicy.DEFAULT; } HedgingPolicy hedgingPolicy = getHedgingPolicyFromConfig(method); verify( hedgingPolicy.equals(HedgingPolicy.DEFAULT) || getRetryPolicyFromConfig(method).equals(RetryPolicy.DEFAULT), "Can not apply both retry and hedging policy for the method '%s'", method); return hedgingPolicy; } } callOptions = callOptions .withOption(RETRY_POLICY_KEY, new DelayedRetryPolicyProvider()) .withOption(HEDGING_POLICY_KEY, new DelayedHedgingPolicyProvider()); } } MethodInfo info = getMethodInfo(method); if (info == null) { return next.newCall(method, callOptions); } if (info.timeoutNanos != null) { Deadline newDeadline = Deadline.after(info.timeoutNanos, TimeUnit.NANOSECONDS); Deadline existingDeadline = callOptions.getDeadline(); // If the new deadline is sooner than the existing deadline, swap them. if (existingDeadline == null || newDeadline.compareTo(existingDeadline) < 0) { callOptions = callOptions.withDeadline(newDeadline); } } if (info.waitForReady != null) { callOptions = info.waitForReady ? callOptions.withWaitForReady() : callOptions.withoutWaitForReady(); } if (info.maxInboundMessageSize != null) { Integer existingLimit = callOptions.getMaxInboundMessageSize(); if (existingLimit != null) { callOptions = callOptions.withMaxInboundMessageSize( Math.min(existingLimit, info.maxInboundMessageSize)); } else { callOptions = callOptions.withMaxInboundMessageSize(info.maxInboundMessageSize); } } if (info.maxOutboundMessageSize != null) { Integer existingLimit = callOptions.getMaxOutboundMessageSize(); if (existingLimit != null) { callOptions = callOptions.withMaxOutboundMessageSize( Math.min(existingLimit, info.maxOutboundMessageSize)); } else { callOptions = callOptions.withMaxOutboundMessageSize(info.maxOutboundMessageSize); } } return next.newCall(method, callOptions); } @CheckForNull private MethodInfo getMethodInfo(MethodDescriptor method) { Map localServiceMethodMap = serviceMethodMap.get(); MethodInfo info = null; if (localServiceMethodMap != null) { info = localServiceMethodMap.get(method.getFullMethodName()); } if (info == null) { Map localServiceMap = serviceMap.get(); if (localServiceMap != null) { info = localServiceMap.get( MethodDescriptor.extractFullServiceName(method.getFullMethodName())); } } return info; } @VisibleForTesting RetryPolicy getRetryPolicyFromConfig(MethodDescriptor method) { MethodInfo info = getMethodInfo(method); return info == null ? RetryPolicy.DEFAULT : info.retryPolicy; } @VisibleForTesting HedgingPolicy getHedgingPolicyFromConfig(MethodDescriptor method) { MethodInfo info = getMethodInfo(method); return info == null ? HedgingPolicy.DEFAULT : info.hedgingPolicy; } }