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

io.envoyproxy.controlplane.server.DiscoveryServer Maven / Gradle / Ivy

package io.envoyproxy.controlplane.server;

import static com.google.common.base.Strings.isNullOrEmpty;

import com.google.protobuf.Any;
import envoy.api.v2.ClusterDiscoveryServiceGrpc.ClusterDiscoveryServiceImplBase;
import envoy.api.v2.Discovery.DiscoveryRequest;
import envoy.api.v2.Discovery.DiscoveryResponse;
import envoy.api.v2.EndpointDiscoveryServiceGrpc.EndpointDiscoveryServiceImplBase;
import envoy.api.v2.ListenerDiscoveryServiceGrpc.ListenerDiscoveryServiceImplBase;
import envoy.api.v2.RouteDiscoveryServiceGrpc.RouteDiscoveryServiceImplBase;
import envoy.service.discovery.v2.AggregatedDiscoveryServiceGrpc.AggregatedDiscoveryServiceImplBase;
import io.envoyproxy.controlplane.cache.ConfigWatcher;
import io.envoyproxy.controlplane.cache.Resources;
import io.envoyproxy.controlplane.cache.Response;
import io.envoyproxy.controlplane.cache.Watch;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;

public class DiscoveryServer {

  private static final DiscoveryServerCallbacks DEFAULT_CALLBACKS = new DiscoveryServerCallbacks() { };
  private static final Logger LOGGER = LoggerFactory.getLogger(DiscoveryServer.class);

  static final String ANY_TYPE_URL = "";

  private final DiscoveryServerCallbacks callbacks;
  private final ConfigWatcher configWatcher;
  private final AtomicLong streamCount = new AtomicLong();

  public DiscoveryServer(DiscoveryServerCallbacks callbacks, ConfigWatcher configWatcher) {
    this.callbacks = callbacks;
    this.configWatcher = configWatcher;
  }

  public DiscoveryServer(ConfigWatcher configWatcher) {
    this(DEFAULT_CALLBACKS, configWatcher);
  }

  /**
   * Returns an ADS implementation that uses this server's {@link ConfigWatcher}.
   */
  public AggregatedDiscoveryServiceImplBase getAggregatedDiscoveryServiceImpl() {
    return new AggregatedDiscoveryServiceImplBase() {
      @Override
      public StreamObserver streamAggregatedResources(
          StreamObserver responseObserver) {

        return createRequestHandler(responseObserver, true, ANY_TYPE_URL);
      }
    };
  }

  /**
   * Returns a CDS implementation that uses this server's {@link ConfigWatcher}.
   */
  public ClusterDiscoveryServiceImplBase getClusterDiscoveryServiceImpl() {
    return new ClusterDiscoveryServiceImplBase() {
      @Override
      public StreamObserver streamClusters(
          StreamObserver responseObserver) {

        return createRequestHandler(responseObserver, false, Resources.CLUSTER_TYPE_URL);
      }
    };
  }

  /**
   * Returns an EDS implementation that uses this server's {@link ConfigWatcher}.
   */
  public EndpointDiscoveryServiceImplBase getEndpointDiscoveryServiceImpl() {
    return new EndpointDiscoveryServiceImplBase() {
      @Override
      public StreamObserver streamEndpoints(
          StreamObserver responseObserver) {

        return createRequestHandler(responseObserver, false, Resources.ENDPOINT_TYPE_URL);
      }
    };
  }

  /**
   * Returns a LDS implementation that uses this server's {@link ConfigWatcher}.
   */
  public ListenerDiscoveryServiceImplBase getListenerDiscoveryServiceImpl() {
    return new ListenerDiscoveryServiceImplBase() {
      @Override
      public StreamObserver streamListeners(
          StreamObserver responseObserver) {

        return createRequestHandler(responseObserver, false, Resources.LISTENER_TYPE_URL);
      }
    };
  }

  /**
   * Returns a RDS implementation that uses this server's {@link ConfigWatcher}.
   */
  public RouteDiscoveryServiceImplBase getRouteDiscoveryServiceImpl() {
    return new RouteDiscoveryServiceImplBase() {
      @Override
      public StreamObserver streamRoutes(
          StreamObserver responseObserver) {

        return createRequestHandler(responseObserver, false, Resources.ROUTE_TYPE_URL);
      }
    };
  }

  private StreamObserver createRequestHandler(
      StreamObserver responseObserver,
      boolean ads,
      String defaultTypeUrl) {

    long streamId = streamCount.getAndIncrement();

    LOGGER.info("[{}] open stream from {}", streamId, defaultTypeUrl);

    callbacks.onStreamOpen(streamId, defaultTypeUrl);

    return new StreamObserver() {

      private final Map watches = new ConcurrentHashMap<>(Resources.TYPE_URLS.size());
      private final Map latestResponse =
          new ConcurrentHashMap<>(Resources.TYPE_URLS.size());
      private final Map> ackedResources = new ConcurrentHashMap<>(Resources.TYPE_URLS.size());

      private AtomicLong streamNonce = new AtomicLong();

      @Override
      public void onNext(DiscoveryRequest request) {
        String nonce = request.getResponseNonce();
        String requestTypeUrl = request.getTypeUrl();

        if (defaultTypeUrl.equals(ANY_TYPE_URL)) {
          if (requestTypeUrl.isEmpty()) {
            responseObserver.onError(
                Status.UNKNOWN
                    .withDescription(String.format("[%d] type URL is required for ADS", streamId))
                    .asRuntimeException());

            return;
          }
        } else if (requestTypeUrl.isEmpty()) {
          requestTypeUrl = defaultTypeUrl;
        }

        LOGGER.info("[{}] request {}[{}] with nonce {} from version {}",
            streamId,
            requestTypeUrl,
            String.join(", ", request.getResourceNamesList()),
            nonce,
            request.getVersionInfo());

        callbacks.onStreamRequest(streamId, request);

        for (String typeUrl : Resources.TYPE_URLS) {
          DiscoveryResponse response = latestResponse.get(typeUrl);
          String resourceNonce = response == null ? null : response.getNonce();

          if (requestTypeUrl.equals(typeUrl) && (isNullOrEmpty(resourceNonce)
              || resourceNonce.equals(nonce))) {
            if (!request.hasErrorDetail() && response != null) {
              Set ackedResourcesForType = response.getResourcesList()
                  .stream()
                  .map(Resources::getResourceName)
                  .collect(Collectors.toSet());
              ackedResources.put(typeUrl, ackedResourcesForType);
            }

            watches.compute(typeUrl, (t, oldWatch) -> {
              if (oldWatch != null) {
                oldWatch.cancel();
              }

              Watch newWatch = configWatcher.createWatch(ads, request,
                  ackedResources.getOrDefault(typeUrl, Collections.emptySet()));

              Flux.from(newWatch.value())
                  .doOnError(e -> responseObserver.onError(
                      Status.UNAVAILABLE
                          .withCause(e)
                          .withDescription(String.format("[%d] %s watch failed", streamId, typeUrl))
                          .asException()))
                  .subscribe(r -> latestResponse.put(typeUrl, send(r, typeUrl)));

              return newWatch;
            });

            return;
          }
        }
      }

      @Override
      public void onError(Throwable t) {
        if (!Status.fromThrowable(t).equals(Status.CANCELLED)) {
          LOGGER.error("[{}] stream closed with error", streamId, t);
        }

        try {
          callbacks.onStreamCloseWithError(streamId, defaultTypeUrl, t);
          responseObserver.onError(Status.fromThrowable(t).asException());
        } finally {
          cancel();
        }
      }

      @Override
      public void onCompleted() {
        LOGGER.info("[{}] stream closed", streamId);

        try {
          callbacks.onStreamClose(streamId, defaultTypeUrl);
          responseObserver.onCompleted();
        } finally {
          cancel();
        }
      }

      private void cancel() {
        watches.values().forEach(Watch::cancel);
      }

      private DiscoveryResponse send(Response response, String typeUrl) {
        String nonce = Long.toString(streamNonce.getAndIncrement());

        DiscoveryResponse discoveryResponse = DiscoveryResponse.newBuilder()
            .setVersionInfo(response.version())
            .addAllResources(response.resources().stream().map(Any::pack).collect(Collectors.toList()))
            .setTypeUrl(typeUrl)
            .setNonce(nonce)
            .build();

        LOGGER.info("[{}] response {} with nonce {} version {}", streamId, typeUrl, nonce, response.version());

        callbacks.onStreamResponse(streamId, response.request(), discoveryResponse);

        // The watch value streams are being observed on multiple threads, so we need to synchronize
        // here because StreamObserver instances are not thread-safe.
        synchronized (responseObserver) {
          responseObserver.onNext(discoveryResponse);
        }

        return discoveryResponse;
      }
    };
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy