Skip to content

Commit 1a649b9

Browse files
author
Lukasz Dziedziak
committed
Send EDS response in ADS mode when Envoy reconnect and asks with Empty version
Signed-off-by: Lukasz Dziedziak <[email protected]>
1 parent 046ab2b commit 1a649b9

File tree

2 files changed

+80
-9
lines changed

2 files changed

+80
-9
lines changed

cache/src/main/java/io/envoyproxy/controlplane/cache/SimpleCache.java

+38-7
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import com.google.common.collect.ImmutableSet;
44
import com.google.common.collect.Sets;
55
import com.google.protobuf.Message;
6+
import io.envoyproxy.envoy.api.v2.ClusterLoadAssignment;
67
import io.envoyproxy.envoy.api.v2.DiscoveryRequest;
78
import java.util.Collection;
9+
import java.util.Collections;
810
import java.util.HashMap;
911
import java.util.Map;
1012
import java.util.Objects;
@@ -18,6 +20,7 @@
1820
import java.util.function.Consumer;
1921
import java.util.stream.Collectors;
2022
import javax.annotation.concurrent.GuardedBy;
23+
2124
import org.slf4j.Logger;
2225
import org.slf4j.LoggerFactory;
2326

@@ -261,21 +264,42 @@ private Response createResponse(DiscoveryRequest request, Map<String, ? extends
261264

262265
private boolean respond(Watch watch, Snapshot snapshot, T group) {
263266
Map<String, ? extends Message> snapshotResources = snapshot.resources(watch.request().getTypeUrl());
267+
Map<String, ClusterLoadAssignment> snapshotWithMissingResources = Collections.emptyMap();
264268

265269
if (!watch.request().getResourceNamesList().isEmpty() && watch.ads()) {
266270
Collection<String> missingNames = watch.request().getResourceNamesList().stream()
267271
.filter(name -> !snapshotResources.containsKey(name))
268272
.collect(Collectors.toList());
269273

270-
if (!missingNames.isEmpty()) {
274+
// When Envoy receive CDS response and reconnect to new instance of control-plane before EDS was sent, Envoy might
275+
// stack in warming phase. New instance of control-plane might not have cluster in snapshot and won't be able to
276+
// respond. First request from Envoy contains empty string version info.
277+
if (!missingNames.isEmpty()
278+
&& watch.request().getTypeUrl().equals(Resources.ENDPOINT_TYPE_URL)
279+
&& watch.request().getVersionInfo().equals("")) {
280+
LOGGER.info("adding missing resources [{}] to response for {} in ADS mode from node {} at version {}",
281+
String.join(", ", missingNames),
282+
watch.request().getTypeUrl(),
283+
group,
284+
snapshot.version(watch.request().getTypeUrl(), watch.request().getResourceNamesList())
285+
);
286+
snapshotWithMissingResources = new HashMap<>(missingNames.size() + snapshotResources.size());
287+
for (String missingName : missingNames) {
288+
snapshotWithMissingResources.put(
289+
missingName,
290+
ClusterLoadAssignment.newBuilder().setClusterName(missingName).build()
291+
);
292+
snapshotWithMissingResources.putAll(
293+
(Map<? extends String, ? extends ClusterLoadAssignment>) snapshotResources);
294+
}
295+
} else if (!missingNames.isEmpty()) {
271296
LOGGER.info(
272297
"not responding in ADS mode for {} from node {} at version {} for request [{}] since [{}] not in snapshot",
273298
watch.request().getTypeUrl(),
274299
group,
275300
snapshot.version(watch.request().getTypeUrl(), watch.request().getResourceNamesList()),
276301
String.join(", ", watch.request().getResourceNamesList()),
277302
String.join(", ", missingNames));
278-
279303
return false;
280304
}
281305
}
@@ -287,11 +311,18 @@ private boolean respond(Watch watch, Snapshot snapshot, T group) {
287311
group,
288312
watch.request().getVersionInfo(),
289313
version);
290-
291-
Response response = createResponse(
292-
watch.request(),
293-
snapshotResources,
294-
version);
314+
Response response;
315+
if (!snapshotWithMissingResources.isEmpty()) {
316+
response = createResponse(
317+
watch.request(),
318+
snapshotWithMissingResources,
319+
version);
320+
} else {
321+
response = createResponse(
322+
watch.request(),
323+
snapshotResources,
324+
version);
325+
}
295326

296327
try {
297328
watch.respond(response);

cache/src/test/java/io/envoyproxy/controlplane/cache/SimpleCacheTest.java

+42-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.envoyproxy.envoy.api.v2.core.Node;
1515
import java.util.Collections;
1616
import java.util.LinkedList;
17+
import java.util.List;
1718
import java.util.Map;
1819
import java.util.UUID;
1920
import java.util.concurrent.ThreadLocalRandom;
@@ -73,13 +74,50 @@ public void invalidNamesListShouldReturnWatcherWithNoResponseInAdsMode() {
7374
.setNode(Node.getDefaultInstance())
7475
.setTypeUrl(Resources.ENDPOINT_TYPE_URL)
7576
.addResourceNames("none")
77+
.setVersionInfo("123")
7678
.build(),
7779
Collections.emptySet(),
7880
responseTracker);
7981

8082
assertThatWatchIsOpenWithNoResponses(new WatchAndTracker(watch, responseTracker));
8183
}
8284

85+
@Test
86+
public void invalidNamesListShouldReturnWatcherWithResponseInAdsModeWhenVersionIsEmpty() {
87+
SimpleCache<String> cache = new SimpleCache<>(new SingleNodeGroup());
88+
89+
cache.setSnapshot(SingleNodeGroup.GROUP, MULTIPLE_RESOURCES_SNAPSHOT2);
90+
91+
ResponseTracker responseTracker = new ResponseTracker();
92+
93+
Watch watch = cache.createWatch(
94+
true,
95+
DiscoveryRequest.newBuilder()
96+
.setNode(Node.getDefaultInstance())
97+
.setTypeUrl(Resources.ENDPOINT_TYPE_URL)
98+
.addAllResourceNames(MULTIPLE_RESOURCES_SNAPSHOT2.resources(Resources.ENDPOINT_TYPE_URL).keySet())
99+
.addResourceNames("none")
100+
.setVersionInfo("")
101+
.build(),
102+
Collections.emptySet(),
103+
responseTracker);
104+
105+
assertThat(responseTracker.responses).isNotEmpty();
106+
107+
Response response = responseTracker.responses.getFirst();
108+
109+
assertThat(response).isNotNull();
110+
assertThat(response.version()).isEqualTo(MULTIPLE_RESOURCES_SNAPSHOT2.version(watch.request().getTypeUrl()));
111+
112+
List<ClusterLoadAssignment> expectedResponse = new LinkedList<>((List<ClusterLoadAssignment>)
113+
MULTIPLE_RESOURCES_SNAPSHOT2.resources(watch.request().getTypeUrl()).values());
114+
// Because versionInfo in request is empty we should send all requested resources to don't leave Envoy
115+
// in warming state.
116+
expectedResponse.add(ClusterLoadAssignment.newBuilder().setClusterName("none").build());
117+
118+
assertThat(response.resources().toArray(new Message[0])).containsExactlyElementsOf(expectedResponse);
119+
}
120+
83121
@Test
84122
public void invalidNamesListShouldReturnWatcherWithResponseInXdsMode() {
85123
SimpleCache<String> cache = new SimpleCache<>(new SingleNodeGroup());
@@ -402,7 +440,8 @@ public void clearSnapshotWithWatches() {
402440
.setTypeUrl("")
403441
.build(),
404442
Collections.emptySet(),
405-
r -> { });
443+
r -> {
444+
});
406445

407446
// clearSnapshot should fail and the snapshot should be left untouched
408447
assertThat(cache.clearSnapshot(SingleNodeGroup.GROUP)).isFalse();
@@ -428,7 +467,8 @@ public void groups() {
428467
.setTypeUrl("")
429468
.build(),
430469
Collections.emptySet(),
431-
r -> { });
470+
r -> {
471+
});
432472

433473
assertThat(cache.groups()).containsExactly(SingleNodeGroup.GROUP);
434474
}

0 commit comments

Comments
 (0)