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

io.grpc.xds.client.ControlPlaneClient Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2020 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.xds.client;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
import com.google.common.base.Supplier;
import com.google.protobuf.Any;
import com.google.rpc.Code;
import io.envoyproxy.envoy.service.discovery.v3.AggregatedDiscoveryServiceGrpc;
import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest;
import io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse;
import io.grpc.InternalLogId;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.grpc.SynchronizationContext;
import io.grpc.SynchronizationContext.ScheduledHandle;
import io.grpc.internal.BackoffPolicy;
import io.grpc.xds.client.Bootstrapper.ServerInfo;
import io.grpc.xds.client.EnvoyProtoData.Node;
import io.grpc.xds.client.XdsClient.ProcessingTracker;
import io.grpc.xds.client.XdsClient.ResourceStore;
import io.grpc.xds.client.XdsClient.XdsResponseHandler;
import io.grpc.xds.client.XdsLogger.XdsLogLevel;
import io.grpc.xds.client.XdsTransportFactory.EventHandler;
import io.grpc.xds.client.XdsTransportFactory.StreamingCall;
import io.grpc.xds.client.XdsTransportFactory.XdsTransport;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;

/**
 * Common base type for XdsClient implementations, which encapsulates the layer abstraction of
 * the xDS RPC stream.
 */
final class ControlPlaneClient {

  private final SynchronizationContext syncContext;
  private final InternalLogId logId;
  private final XdsLogger logger;
  private final ServerInfo serverInfo;
  private final XdsTransport xdsTransport;
  private final XdsResponseHandler xdsResponseHandler;
  private final ResourceStore resourceStore;
  private final ScheduledExecutorService timeService;
  private final BackoffPolicy.Provider backoffPolicyProvider;
  private final Stopwatch stopwatch;
  private final Node bootstrapNode;
  private final XdsClient xdsClient;

  // Last successfully applied version_info for each resource type. Starts with empty string.
  // A version_info is used to update management server with client's most recent knowledge of
  // resources.
  private final Map, String> versions = new HashMap<>();

  private boolean shutdown;
  private boolean streamClosedNoResponse;
  @Nullable
  private AdsStream adsStream;
  @Nullable
  private BackoffPolicy retryBackoffPolicy;
  @Nullable
  private ScheduledHandle rpcRetryTimer;
  private MessagePrettyPrinter messagePrinter;

  /** An entity that manages ADS RPCs over a single channel. */
  ControlPlaneClient(
      XdsTransport xdsTransport,
      ServerInfo serverInfo,
      Node bootstrapNode,
      XdsResponseHandler xdsResponseHandler,
      ResourceStore resourceStore,
      ScheduledExecutorService
      timeService,
      SynchronizationContext syncContext,
      BackoffPolicy.Provider backoffPolicyProvider,
      Supplier stopwatchSupplier,
      XdsClient xdsClient,
      MessagePrettyPrinter messagePrinter) {
    this.serverInfo = checkNotNull(serverInfo, "serverInfo");
    this.xdsTransport = checkNotNull(xdsTransport, "xdsTransport");
    this.xdsResponseHandler = checkNotNull(xdsResponseHandler, "xdsResponseHandler");
    this.resourceStore = checkNotNull(resourceStore, "resourcesSubscriber");
    this.bootstrapNode = checkNotNull(bootstrapNode, "bootstrapNode");
    this.timeService = checkNotNull(timeService, "timeService");
    this.syncContext = checkNotNull(syncContext, "syncContext");
    this.backoffPolicyProvider = checkNotNull(backoffPolicyProvider, "backoffPolicyProvider");
    this.xdsClient = checkNotNull(xdsClient, "xdsClient");
    this.messagePrinter = checkNotNull(messagePrinter, "messagePrinter");
    stopwatch = checkNotNull(stopwatchSupplier, "stopwatchSupplier").get();
    logId = InternalLogId.allocate("xds-client", serverInfo.target());
    logger = XdsLogger.withLogId(logId);
    logger.log(XdsLogLevel.INFO, "Created");
  }

  void shutdown() {
    syncContext.execute(new Runnable() {
      @Override
      public void run() {
        shutdown = true;
        logger.log(XdsLogLevel.INFO, "Shutting down");
        if (adsStream != null) {
          adsStream.close(Status.CANCELLED.withDescription("shutdown").asException());
        }
        if (rpcRetryTimer != null && rpcRetryTimer.isPending()) {
          rpcRetryTimer.cancel();
        }
        xdsTransport.shutdown();
      }
    });
  }

  @Override
  public String toString() {
    return logId.toString();
  }

  /**
   * Updates the resource subscription for the given resource type.
   */
  // Must be synchronized.
  void adjustResourceSubscription(XdsResourceType resourceType) {
    if (isInBackoff()) {
      return;
    }
    if (adsStream == null) {
      startRpcStream();
    }
    Collection resources = resourceStore.getSubscribedResources(serverInfo, resourceType);
    if (resources == null) {
      resources = Collections.emptyList();
    }
    adsStream.sendDiscoveryRequest(resourceType, resources);
    if (resources.isEmpty()) {
      // The resource type no longer has subscribing resources; clean up references to it, except
      // for nonces. If the resource type becomes used again the control plane can ignore requests
      // for old/missing nonces. Old type's nonces are dropped when the ADS stream is restarted.
      versions.remove(resourceType);
    }
  }

  /**
   * Accepts the update for the given resource type by updating the latest resource version
   * and sends an ACK request to the management server.
   */
  // Must be synchronized.
  void ackResponse(XdsResourceType type, String versionInfo, String nonce) {
    versions.put(type, versionInfo);
    logger.log(XdsLogLevel.INFO, "Sending ACK for {0} update, nonce: {1}, current version: {2}",
        type.typeName(), nonce, versionInfo);
    Collection resources = resourceStore.getSubscribedResources(serverInfo, type);
    if (resources == null) {
      resources = Collections.emptyList();
    }
    adsStream.sendDiscoveryRequest(type, versionInfo, resources, nonce, null);
  }

  /**
   * Rejects the update for the given resource type and sends an NACK request (request with last
   * accepted version) to the management server.
   */
  // Must be synchronized.
  void nackResponse(XdsResourceType type, String nonce, String errorDetail) {
    String versionInfo = versions.getOrDefault(type, "");
    logger.log(XdsLogLevel.INFO, "Sending NACK for {0} update, nonce: {1}, current version: {2}",
        type.typeName(), nonce, versionInfo);
    Collection resources = resourceStore.getSubscribedResources(serverInfo, type);
    if (resources == null) {
      resources = Collections.emptyList();
    }
    adsStream.sendDiscoveryRequest(type, versionInfo, resources, nonce, errorDetail);
  }

  /**
   * Returns {@code true} if the resource discovery is currently in backoff.
   */
  // Must be synchronized.
  boolean isInBackoff() {
    return rpcRetryTimer != null && rpcRetryTimer.isPending();
  }

  // Must be synchronized.
  boolean isReady() {
    return adsStream != null && adsStream.call != null && adsStream.call.isReady();
  }

  /**
   * Starts a timer for each requested resource that hasn't been responded to and
   * has been waiting for the channel to get ready.
   */
  // Must be synchronized.
  void readyHandler() {
    if (!isReady()) {
      return;
    }

    if (isInBackoff()) {
      rpcRetryTimer.cancel();
      rpcRetryTimer = null;
    }

    xdsClient.startSubscriberTimersIfNeeded(serverInfo);
  }

  /**
   * Indicates whether there is an active ADS stream.
   *
   * 

Return {@code true} when the {@code AdsStream} is created. * {@code false} when the ADS stream fails without a response. Resets to true * upon receiving the first response on a new ADS stream. */ // Must be synchronized boolean hasWorkingAdsStream() { return !streamClosedNoResponse; } /** * Establishes the RPC connection by creating a new RPC stream on the given channel for * xDS protocol communication. */ // Must be synchronized. private void startRpcStream() { checkState(adsStream == null, "Previous adsStream has not been cleared yet"); adsStream = new AdsStream(); logger.log(XdsLogLevel.INFO, "ADS stream started"); stopwatch.reset().start(); } @VisibleForTesting public final class RpcRetryTask implements Runnable { @Override public void run() { if (shutdown) { return; } startRpcStream(); Set> subscribedResourceTypes = new HashSet<>(resourceStore.getSubscribedResourceTypesWithTypeUrl().values()); for (XdsResourceType type : subscribedResourceTypes) { Collection resources = resourceStore.getSubscribedResources(serverInfo, type); if (resources != null) { adsStream.sendDiscoveryRequest(type, resources); } } xdsResponseHandler.handleStreamRestarted(serverInfo); } } @VisibleForTesting @Nullable XdsResourceType fromTypeUrl(String typeUrl) { return resourceStore.getSubscribedResourceTypesWithTypeUrl().get(typeUrl); } private class AdsStream implements EventHandler { private boolean responseReceived; private boolean closed; // Response nonce for the most recently received discovery responses of each resource type URL. // Client initiated requests start response nonce with empty string. // Nonce in each response is echoed back in the following ACK/NACK request. It is // used for management server to identify which response the client is ACKing/NACking. // To avoid confusion, client-initiated requests will always use the nonce in // most recently received responses of each resource type. Nonces are never deleted from the // map; nonces are only discarded once the stream closes because xds_protocol says "the // management server should not send a DiscoveryResponse for any DiscoveryRequest that has a // stale nonce." private final Map respNonces = new HashMap<>(); private final StreamingCall call; private final MethodDescriptor methodDescriptor = AggregatedDiscoveryServiceGrpc.getStreamAggregatedResourcesMethod(); private AdsStream() { this.call = xdsTransport.createStreamingCall(methodDescriptor.getFullMethodName(), methodDescriptor.getRequestMarshaller(), methodDescriptor.getResponseMarshaller()); call.start(this); } /** * Sends a discovery request with the given {@code versionInfo}, {@code nonce} and * {@code errorDetail}. Used for reacting to a specific discovery response. For * client-initiated discovery requests, use {@link * #sendDiscoveryRequest(XdsResourceType, Collection)}. */ void sendDiscoveryRequest(XdsResourceType type, String versionInfo, Collection resources, String nonce, @Nullable String errorDetail) { DiscoveryRequest.Builder builder = DiscoveryRequest.newBuilder() .setVersionInfo(versionInfo) .setNode(bootstrapNode.toEnvoyProtoNode()) .addAllResourceNames(resources) .setTypeUrl(type.typeUrl()) .setResponseNonce(nonce); if (errorDetail != null) { com.google.rpc.Status error = com.google.rpc.Status.newBuilder() .setCode(Code.INVALID_ARGUMENT_VALUE) // FIXME(chengyuanzhang): use correct code .setMessage(errorDetail) .build(); builder.setErrorDetail(error); } DiscoveryRequest request = builder.build(); call.sendMessage(request); if (logger.isLoggable(XdsLogLevel.DEBUG)) { logger.log(XdsLogLevel.DEBUG, "Sent DiscoveryRequest\n{0}", messagePrinter.print(request)); } } /** * Sends a client-initiated discovery request. */ final void sendDiscoveryRequest(XdsResourceType type, Collection resources) { logger.log(XdsLogLevel.INFO, "Sending {0} request for resources: {1}", type, resources); sendDiscoveryRequest(type, versions.getOrDefault(type, ""), resources, respNonces.getOrDefault(type.typeUrl(), ""), null); } @Override public void onReady() { syncContext.execute(ControlPlaneClient.this::readyHandler); } @Override public void onRecvMessage(DiscoveryResponse response) { syncContext.execute(new Runnable() { @Override public void run() { // Reset flag as message has been received on a stream streamClosedNoResponse = false; respNonces.put(response.getTypeUrl(), response.getNonce()); XdsResourceType type = fromTypeUrl(response.getTypeUrl()); if (logger.isLoggable(XdsLogLevel.DEBUG)) { logger.log( XdsLogLevel.DEBUG, "Received {0} response:\n{1}", type, messagePrinter.print(response)); } if (type == null) { logger.log( XdsLogLevel.WARNING, "Ignore an unknown type of DiscoveryResponse: {0}", response.getTypeUrl()); call.startRecvMessage(); return; } handleRpcResponse(type, response.getVersionInfo(), response.getResourcesList(), response.getNonce()); } }); } @Override public void onStatusReceived(final Status status) { syncContext.execute(() -> { handleRpcStreamClosed(status); }); } final void handleRpcResponse(XdsResourceType type, String versionInfo, List resources, String nonce) { checkNotNull(type, "type"); if (closed) { return; } responseReceived = true; ProcessingTracker processingTracker = new ProcessingTracker( () -> call.startRecvMessage(), syncContext); xdsResponseHandler.handleResourceResponse(type, serverInfo, versionInfo, resources, nonce, processingTracker); processingTracker.onComplete(); } private void handleRpcStreamClosed(Status status) { if (closed) { return; } if (responseReceived || retryBackoffPolicy == null) { // Reset the backoff sequence if had received a response, or backoff sequence // has never been initialized. retryBackoffPolicy = backoffPolicyProvider.get(); } // FakeClock in tests isn't thread-safe. Schedule the retry timer before notifying callbacks // to avoid TSAN races, since tests may wait until callbacks are called but then would run // concurrently with the stopwatch and schedule. long elapsed = stopwatch.elapsed(TimeUnit.NANOSECONDS); long delayNanos = Math.max(0, retryBackoffPolicy.nextBackoffNanos() - elapsed); rpcRetryTimer = syncContext.schedule( new RpcRetryTask(), delayNanos, TimeUnit.NANOSECONDS, timeService); Status newStatus = status; if (responseReceived) { // A closed ADS stream after a successful response is not considered an error. Servers may // close streams for various reasons during normal operation, such as load balancing or // underlying connection hitting its max connection age limit (see gRFC A9). if (!status.isOk()) { newStatus = Status.OK; logger.log( XdsLogLevel.DEBUG, "ADS stream closed with error {0}: {1}. However, a " + "response was received, so this will not be treated as an error. Cause: {2}", status.getCode(), status.getDescription(), status.getCause()); } else { logger.log(XdsLogLevel.DEBUG, "ADS stream closed by server after a response was received"); } } else { streamClosedNoResponse = true; // If the ADS stream is closed without ever having received a response from the server, then // the XdsClient should consider that a connectivity error (see gRFC A57). if (status.isOk()) { newStatus = Status.UNAVAILABLE.withDescription( "ADS stream closed with OK before receiving a response"); } logger.log( XdsLogLevel.ERROR, "ADS stream failed with status {0}: {1}. Cause: {2}", newStatus.getCode(), newStatus.getDescription(), newStatus.getCause()); } closed = true; xdsResponseHandler.handleStreamClosed(newStatus); cleanUp(); logger.log(XdsLogLevel.INFO, "Retry ADS stream in {0} ns", delayNanos); } private void close(Exception error) { if (closed) { return; } closed = true; cleanUp(); call.sendError(error); } private void cleanUp() { if (adsStream == this) { adsStream = null; } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy