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

pl.allegro.tech.servicemesh.envoycontrol.SimpleCache Maven / Gradle / Ivy

There is a newer version: 0.22.4
Show newest version
package pl.allegro.tech.servicemesh.envoycontrol;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.protobuf.Message;
import io.envoyproxy.controlplane.cache.*;
import io.envoyproxy.controlplane.cache.GroupCacheStatusInfo;
import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.concurrent.GuardedBy;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.function.Function;
import java.util.stream.Stream;

import static io.envoyproxy.controlplane.cache.Resources.RESOURCE_TYPES_IN_ORDER;

/**
 * This class is copy of {@link io.envoyproxy.controlplane.cache.SimpleCache}
 */
public class SimpleCache implements SnapshotCache {

    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleCache.class);

    private final NodeGroup groups;
    private final boolean shouldSendMissingEndpoints;

    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    @GuardedBy("lock")
    private final Map snapshots = new HashMap<>();
    private final CacheStatusInfoAggregator statuses = new CacheStatusInfoAggregator<>();

    private AtomicLong watchCount = new AtomicLong();

    /**
     * Constructs a simple cache.
     *
     * @param groups                     maps an envoy host to a node group
     * @param shouldSendMissingEndpoints if set to true it will respond with empty endpoints if there is no in snapshot
     */
    public SimpleCache(NodeGroup groups, boolean shouldSendMissingEndpoints) {
        this.groups = groups;
        this.shouldSendMissingEndpoints = shouldSendMissingEndpoints;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean clearSnapshot(T group) {
        // we take a writeLock to prevent watches from being created
        writeLock.lock();
        try {

            // If we don't know about this group, do nothing.
            if (statuses.hasStatuses(group)) {
                LOGGER.warn("tried to clear snapshot for group with existing watches, group={}", group);

                return false;
            }

            statuses.remove(group);
            snapshots.remove(group);

            return true;
        } finally {
            writeLock.unlock();
        }
    }

  public Watch createWatch(
      boolean ads,
      XdsRequest request,
      Set knownResourceNames,
      Consumer responseConsumer) {
    return createWatch(ads, request, knownResourceNames, responseConsumer, false, false);
  }

    /**
     * {@inheritDoc}
     */
    @Override
    public Watch createWatch(
            boolean ads,
            XdsRequest request,
            Set knownResourceNames,
            Consumer responseConsumer,
            boolean hasClusterChanged,
            boolean allowDefaultEmptyEdsUpdate) {
    Resources.ResourceType requestResourceType = request.getResourceType();
        Preconditions.checkNotNull(requestResourceType, "unsupported type URL %s",
                request.getTypeUrl());
        T group;
        group = groups.hash(request.v3Request().getNode());

        // even though we're modifying, we take a readLock to allow multiple watches to be created in parallel since it
        // doesn't conflict
        readLock.lock();
        try {
            CacheStatusInfo status = statuses.getOrAddStatusInfo(group, requestResourceType);
            status.setLastWatchRequestTime(System.currentTimeMillis());

            U snapshot = snapshots.get(group);
            String version = snapshot == null ? "" : snapshot.version(requestResourceType, request.getResourceNamesList());

            Watch watch = new Watch(ads, allowDefaultEmptyEdsUpdate, request, responseConsumer);

            if (snapshot != null) {
                Set requestedResources = ImmutableSet.copyOf(request.getResourceNamesList());

                // If the request is asking for resources we haven't sent to the proxy yet, see if we have additional resources.
                if (!knownResourceNames.equals(requestedResources)) {
                    Sets.SetView newResourceHints = Sets.difference(requestedResources, knownResourceNames);

                    // If any of the newly requested resources are in the snapshot respond immediately. If not we'll fall back to
                    // version comparisons.
                    if (snapshot.resources(requestResourceType)
                            .keySet()
                            .stream()
                            .anyMatch(newResourceHints::contains)) {
                        respond(watch, snapshot, group);

                        return watch;
                    }
                } else if (hasClusterChanged && requestResourceType.equals(Resources.ResourceType.ENDPOINT)) {
                    respond(watch, snapshot, group);

                    return watch;
                }
            }

            // If the requested version is up-to-date or missing a response, leave an open watch.
            if (snapshot == null || request.getVersionInfo().equals(version)) {
                openWatch(status, watch, request.getTypeUrl(), request.getResourceNamesList(), group, request.getVersionInfo());

                return watch;
            }

            // Otherwise, the watch may be responded immediately
            boolean responded = respond(watch, snapshot, group);

            if (!responded) {
                openWatch(status, watch, request.getTypeUrl(), request.getResourceNamesList(), group, request.getVersionInfo());
            }

            return watch;
        } finally {
            readLock.unlock();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DeltaWatch createDeltaWatch(
        DeltaXdsRequest request,
        String requesterVersion,
        Map resourceVersions,
        Set pendingResources,
        boolean isWildcard,
        Consumer responseConsumer,
        boolean hasClusterChanged) {

        Resources.ResourceType requestResourceType = request.getResourceType();
        Preconditions.checkNotNull(requestResourceType, "unsupported type URL %s",
            request.getTypeUrl());
        T group;
        group = groups.hash(request.v3Request().getNode());

        // even though we're modifying, we take a readLock to allow multiple watches to be created in parallel since it
        // doesn't conflict
        readLock.lock();
        try {
            DeltaCacheStatusInfo status = statuses.getOrAddDeltaStatusInfo(group, requestResourceType);

            status.setLastWatchRequestTime(System.currentTimeMillis());

            U snapshot = snapshots.get(group);
            String version = snapshot == null ? "" : snapshot.version(requestResourceType, Collections.emptyList());

            DeltaWatch watch = new DeltaWatch(request,
                ImmutableMap.copyOf(resourceVersions),
                ImmutableSet.copyOf(pendingResources),
                requesterVersion,
                isWildcard,
                responseConsumer);

            // If no snapshot, leave an open watch.

            if (snapshot == null) {
                openWatch(status, watch, request.getTypeUrl(),  watch.trackedResources().keySet(), group, requesterVersion);
                return watch;
            }

            // If the requested version is up-to-date or missing a response, leave an open watch.
            if (version.equals(requesterVersion)) {
                // If the request is not wildcard, we have pending resources and we have them, we should respond immediately.
                if (!isWildcard && watch.pendingResources().size() != 0) {
                    // If any of the pending resources are in the snapshot respond immediately. If not we'll fall back to
                    // version comparisons.
                    Map> resources = snapshot.versionedResources(request.getResourceType());
                    Map> requestedResources = watch.pendingResources()
                        .stream()
                        .filter(resources::containsKey)
                        .collect(Collectors.toMap(Function.identity(), resources::get));
                    ResponseState responseState = respondDelta(watch,
                        requestedResources,
                        Collections.emptyList(),
                        version,
                        group);
                    if (responseState.isFinished()) {
                        return watch;
                    }
                } else if (hasClusterChanged && requestResourceType.equals(Resources.ResourceType.ENDPOINT)) {
                    ResponseState responseState = respondDelta(request, watch, snapshot, version, group);
                    if (responseState.isFinished()) {
                        return watch;
                    }
                }

                openWatch(status, watch, request.getTypeUrl(),  watch.trackedResources().keySet(), group, requesterVersion);

                return watch;
            }

            // Otherwise, version is different, the watch may be responded immediately
            ResponseState responseState = respondDelta(request, watch, snapshot, version, group);

            if (responseState.isFinished()) {
                return watch;
            }

            openWatch(status, watch, request.getTypeUrl(),  watch.trackedResources().keySet(), group, requesterVersion);

            return watch;
        } finally {
            readLock.unlock();
        }
    }

    private > void openWatch(MutableStatusInfo status,
                                                           V watch,
                                                           String url,
                                                           Collection resources,
                                                           T group,
                                                           String version) {
        long watchId = watchCount.incrementAndGet();
        status.setWatch(watchId, watch);
        watch.setStop(() -> {
            LOGGER.debug("removing watch {}", watchId);
            status.removeWatch(watchId);
        });

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("open watch {} for {} from node {} for version {}",
                watchId,
                url,
                group,
                version);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public U getSnapshot(T group) {
        readLock.lock();

        try {
            return snapshots.get(group);
        } finally {
            readLock.unlock();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Collection groups() {
        return ImmutableSet.copyOf(statuses.groups());
    }

    /**
     * {@inheritDoc}
     *
     * 

This method cannot be called concurrently for the same group. * It can be called concurrently for different groups. */ @Override public void setSnapshot(T group, U snapshot) { // we take a writeLock to prevent watches from being created while we update the snapshot Map> status; Map> deltaStatus; U previousSnapshot; writeLock.lock(); try { // Update the existing snapshot entry. previousSnapshot = snapshots.put(group, snapshot); status = statuses.getStatus(group); deltaStatus = statuses.getDeltaStatus(group); } finally { writeLock.unlock(); } if (status.isEmpty() && deltaStatus.isEmpty()) { return; } // Responses should be in specific order and typeUrls has a list of resources in the right // order. respondWithSpecificOrder(group, previousSnapshot, snapshot, status, deltaStatus); } /** * {@inheritDoc} */ @Override public StatusInfo statusInfo(T group) { readLock.lock(); try { Map> statusMap = statuses.getStatus(group); Map> deltaStatusMap = statuses.getDeltaStatus(group); if (statusMap.isEmpty() && deltaStatusMap.isEmpty()) { return null; } List> collection = Stream.concat(statusMap.values().stream(), deltaStatusMap.values().stream()).collect(Collectors.toList()); return new GroupCacheStatusInfo<>(collection); } finally { readLock.unlock(); } } @VisibleForTesting protected void respondWithSpecificOrder(T group, U previousSnapshot, U snapshot, Map> statusMap, Map> deltaStatusMap) { for (Resources.ResourceType resourceType : RESOURCE_TYPES_IN_ORDER) { CacheStatusInfo status = statusMap.get(resourceType); if (status != null) { status.watchesRemoveIf((id, watch) -> { if (!watch.request().getResourceType().equals(resourceType)) { return false; } String version = snapshot.version(watch.request().getResourceType(), watch.request().getResourceNamesList()); if (!watch.request().getVersionInfo().equals(version)) { respond(watch, snapshot, group); // Discard the watch. A new watch will be created for future snapshots once envoy ACKs the response. return true; } // Do not discard the watch. The request version is the same as the snapshot version, so we wait to respond. return false; }); } DeltaCacheStatusInfo deltaStatus = deltaStatusMap.get(resourceType); if (deltaStatus != null) { Map> previousResources = previousSnapshot == null ? Collections.emptyMap() : previousSnapshot.versionedResources(resourceType); Map> snapshotResources = snapshot.versionedResources(resourceType); Map> snapshotChangedResources = snapshotResources.entrySet() .stream() .filter(entry -> { VersionedResource versionedResource = previousResources.get(entry.getKey()); return versionedResource == null || !versionedResource .version().equals(entry.getValue().version()); }) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); Set snapshotRemovedResources = previousResources.keySet() .stream() .filter(s -> !snapshotResources.containsKey(s)) .collect(Collectors.toSet()); deltaStatus.watchesRemoveIf((id, watch) -> { String version = snapshot.version(watch.request().getResourceType(), Collections.emptyList()); if (!watch.version().equals(version)) { List removedResources = snapshotRemovedResources.stream() .filter(s -> watch.trackedResources().get(s) != null) .collect(Collectors.toList()); Map> changedResources = findChangedResources(watch, snapshotChangedResources); ResponseState responseState = respondDelta(watch, changedResources, removedResources, version, group); // Discard the watch if it was responded or cancelled. // A new watch will be created for future snapshots once envoy ACKs the response. return responseState.isFinished(); } // Do not discard the watch. The request version is the same as the snapshot version, so we wait to respond. return false; }); } } } private Response createResponse(XdsRequest request, Map> resources, String version) { Collection filtered = request.getResourceNamesList().isEmpty() ? resources.values().stream() .map(VersionedResource::resource) .collect(Collectors.toList()) : request.getResourceNamesList().stream() .map(resources::get) .filter(Objects::nonNull) .map(VersionedResource::resource) .collect(Collectors.toList()); return Response.create(request, filtered, version); } private boolean respond(Watch watch, U snapshot, T group) { Map> snapshotResources = snapshot.versionedResources(watch.request().getResourceType()); Map> snapshotForMissingResources = Collections.emptyMap(); if (!watch.request().getResourceNamesList().isEmpty() && watch.ads()) { Collection missingNames = watch.request().getResourceNamesList().stream() .filter(name -> !snapshotResources.containsKey(name)) .collect(Collectors.toList()); if (!missingNames.isEmpty()) { // In some cases Envoy might send EDS request with cluster names we don't have in snapshot. // This may happen when for example Envoy disconnects from an instance of control-plane and connects to // other instance. // // If shouldSendMissingEndpoints is set to false we will not respond to such request. It may cause // Envoy to stop working correctly, because it will wait indefinitely for a response, // not accepting any other updates. // // If shouldSendMissingEndpoints is set to true, we will respond to such request anyway, to prevent // such problems with Envoy. if (shouldSendMissingEndpoints && watch.request().getResourceType().equals(Resources.ResourceType.ENDPOINT)) { LOGGER.info("adding missing resources [{}] to response for {} in ADS mode from node {} at version {}", String.join(", ", missingNames), watch.request().getTypeUrl(), group, snapshot.version(watch.request().getResourceType(), watch.request().getResourceNamesList()) ); snapshotForMissingResources = new HashMap<>(missingNames.size()); for (String missingName : missingNames) { snapshotForMissingResources.put( missingName, VersionedResource.create(ClusterLoadAssignment.newBuilder().setClusterName(missingName).build()) ); } } else { LOGGER.info( "not responding in ADS mode for {} from node {} at version {} for request [{}] since [{}] not in snapshot", watch.request().getTypeUrl(), group, snapshot.version(watch.request().getResourceType(), watch.request().getResourceNamesList()), String.join(", ", watch.request().getResourceNamesList()), String.join(", ", missingNames)); return false; } } } String version = snapshot.version(watch.request().getResourceType(), watch.request().getResourceNamesList()); LOGGER.debug("responding for {} from node {} at version {} with version {}", watch.request().getTypeUrl(), group, watch.request().getVersionInfo(), version); Response response; if (!snapshotForMissingResources.isEmpty()) { snapshotForMissingResources.putAll(snapshotResources); response = createResponse( watch.request(), snapshotForMissingResources, version); } else { response = createResponse( watch.request(), snapshotResources, version); } try { watch.respond(response); return true; } catch (WatchCancelledException e) { LOGGER.error( "failed to respond for {} from node {} at version {} with version {} because watch was already cancelled", watch.request().getTypeUrl(), group, watch.request().getVersionInfo(), version); } return false; } private List findRemovedResources(DeltaWatch watch, Map> snapshotResources) { // remove resources for which client has a tracked version but do not exist in snapshot return watch.trackedResources().keySet() .stream() .filter(s -> !snapshotResources.containsKey(s)) .collect(Collectors.toList()); } private Map> findChangedResources(DeltaWatch watch, Map> snapshotResources) { return snapshotResources.entrySet() .stream() .filter(entry -> { if (watch.pendingResources().contains(entry.getKey())) { return true; } String resourceVersion = watch.trackedResources().get(entry.getKey()); if (resourceVersion == null) { // resource is not tracked, should respond it only if watch is wildcard return watch.isWildcard(); } return !entry.getValue().version().equals(resourceVersion); }) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } private ResponseState respondDelta(DeltaXdsRequest request, DeltaWatch watch, U snapshot, String version, T group) { Map> snapshotResources = snapshot.versionedResources(request.getResourceType()); List removedResources = findRemovedResources(watch, snapshotResources); Map> changedResources = findChangedResources(watch, snapshotResources); return respondDelta(watch, changedResources, removedResources, version, group); } private ResponseState respondDelta(DeltaWatch watch, Map> resources, List removedResources, String version, T group) { if (resources.isEmpty() && removedResources.isEmpty()) { return ResponseState.UNRESPONDED; } DeltaResponse response = DeltaResponse.create( watch.request(), resources, removedResources, version); try { watch.respond(response); return ResponseState.RESPONDED; } catch (WatchCancelledException e) { LOGGER.error( "failed to respond for {} from node {} with version {} because watch was already cancelled", watch.request().getTypeUrl(), group, version); } return ResponseState.CANCELLED; } private enum ResponseState { RESPONDED, UNRESPONDED, CANCELLED; private boolean isFinished() { return this.equals(RESPONDED) || this.equals(CANCELLED); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy