com.transferwise.envoy.xds.delta.IncrementalDiscoveryService Maven / Gradle / Ivy
The newest version!
package com.transferwise.envoy.xds.delta;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.protobuf.Message;
import com.transferwise.envoy.xds.AbstractDiscoveryService;
import com.transferwise.envoy.xds.ClientNackException;
import com.transferwise.envoy.xds.NodeConfig;
import com.transferwise.envoy.xds.TypeUrl;
import com.transferwise.envoy.xds.api.IncrementalConfigBuilder;
import io.envoyproxy.envoy.service.discovery.v3.DeltaDiscoveryRequest;
import io.envoyproxy.envoy.service.discovery.v3.DeltaDiscoveryResponse;
import io.envoyproxy.envoy.service.discovery.v3.Resource;
import io.grpc.stub.StreamObserver;
import java.util.Collection;
import java.util.HashSet;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
class IncrementalDiscoveryService extends AbstractDiscoveryService {
private final StreamObserver responseObserver;
private String lastNonce = null;
private Long version = 0L;
private final SubManager subManager;
IncrementalDiscoveryService(TypeUrl myTypeUrl, StreamObserver responseObserver, IncrementalConfigBuilder configBuilder, NodeConfig nodeConfig,
SubManager subManager) {
super(myTypeUrl, configBuilder, nodeConfig);
this.responseObserver = responseObserver;
this.subManager = subManager;
}
@Override
protected void processRequest(DeltaDiscoveryRequest value) {
boolean isAck = !Strings.isNullOrEmpty(value.getResponseNonce());
if (isAck) {
// Envoy is trying to ACK something.
if (value.hasErrorDetail()) {
throw new ClientNackException(value.getResponseNonce(), value.getErrorDetail());
}
if (value.getResponseNonce().equals(lastNonce)) {
log.debug("{} ACKed {}", getTypeUrl().name(), lastNonce);
lastNonce = null; // acked!
}
}
ImmutableSet subscribeSet = ImmutableSet.copyOf(value.getResourceNamesSubscribeList());
ImmutableSet unsubscribeSet = ImmutableSet.copyOf(value.getResourceNamesUnsubscribeList());
subManager.processResourceListChange(subscribeSet, unsubscribeSet).ifPresent(newSubFilter -> {
// Subscriptions changed.
if (isAck) {
// The xDS docs suggest that envoy won't both ack something and change subscriptions lists in the same request.
// But I don't entirely trust that it won't. So we support that case, but log about it.
log.info("Client included subscriptions changes in an ack!");
}
processSubUpdate(subscribeSet, newSubFilter, ImmutableSet.copyOf(value.getInitialResourceVersionsMap().keySet()));
});
}
@Override
public Predicate subFilter() {
return subManager::isSubscribedTo;
}
private void processSubUpdate(ImmutableSet newSubs, Predicate filter, ImmutableSet initialState) {
log.debug("{} subscription change - added: {}", getTypeUrl().name(), newSubs);
// Envoy will want us to tell it about everything it's just asked for, assuming it successfully subscribed to it.
// If the resource doesn't exist, however, we will need to tell it to delete it.
// We check against the filter because some values in resource name lists have special meaning to sub management (e.g. wildcards),
// they're not actually resource names.
HashSet removed = newSubs.stream().filter(filter).collect(Collectors.toCollection(HashSet::new));
// If envoy sent us an initial resource versions map it might also contain things that don't exist anymore.
// Again, we will need to tell envoy to delete it, but only if envoy has subscribed for updates about it.
// This is necessary because envoy doesn't explicitly subscribe to named resources (e.g. when it uses wildcards), so it can be
// subscribed to things it has never explicitly asked us for.
initialState.stream().filter(filter).forEach(removed::add);
// Now actually generate the resources for this sub update.
IncrementalConfigBuilder.Resources resources = getResources(filter);
for (IncrementalConfigBuilder.NamedMessage msg: resources.getResources()) {
// If we generated a resource we don't want to tell envoy to remove it.
removed.remove(msg.getName());
}
pushResources(resources.getResources(), removed);
}
@Override
public boolean awaitingAck() {
return lastNonce != null;
}
@Override
public void pushNewState(IncrementalConfigBuilder.Response response) {
if (response.getAddAndUpdates().isEmpty() && response.getRemoves().isEmpty()) {
return;
}
pushResources(response.getAddAndUpdates(), response.getRemoves());
}
private void pushResources(Collection> resources, Collection removals) {
Long myVersion = ++version;
DeltaDiscoveryResponse.Builder responseBuilder = DeltaDiscoveryResponse.newBuilder();
for (IncrementalConfigBuilder.NamedMessage extends Message> namedMessage : resources) {
responseBuilder.addResources(Resource.newBuilder()
.setName(namedMessage.getName())
.setVersion(myVersion.toString())
.setResource(pack(namedMessage.getMessage()))
.build());
}
responseBuilder.addAllRemovedResources(removals);
responseBuilder.setTypeUrl(getTypeUrl().getTypeUrl());
lastNonce = UUID.randomUUID().toString();
responseBuilder.setNonce(lastNonce);
responseBuilder.setSystemVersionInfo(myVersion.toString());
DeltaDiscoveryResponse discoveryResponse = responseBuilder.build();
if (log.isDebugEnabled()) {
log.debug("{} Pushing update {} change - added: {} removed: {}", getTypeUrl().name(), lastNonce, resources.stream().map(IncrementalConfigBuilder.NamedMessage::getName).collect(Collectors.toList()), removals);
}
responseObserver.onNext(discoveryResponse);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy