From 6e2238f231ec936d1f0f71da014526a6f6486730 Mon Sep 17 00:00:00 2001
From: Fahim Farook
Date: Sat, 9 Dec 2017 04:37:05 +0530
Subject: [PATCH] Update Eureka Server's service-urls upon
EnvironmentChangeEvent. (#2455)
Fixes gh-2421
---
.../server/EurekaServerAutoConfiguration.java | 66 ++++-
.../RefreshablePeerEurekaNodesTests.java | 261 ++++++++++++++++++
2 files changed, 325 insertions(+), 2 deletions(-)
create mode 100644 spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/RefreshablePeerEurekaNodesTests.java
diff --git a/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/EurekaServerAutoConfiguration.java b/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/EurekaServerAutoConfiguration.java
index 259350c8..859d1f69 100644
--- a/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/EurekaServerAutoConfiguration.java
+++ b/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/EurekaServerAutoConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2015 the original author or authors.
+ * Copyright 2013-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,7 +35,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.cloud.client.actuator.HasFeatures;
+import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.netflix.eureka.EurekaConstants;
+import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
@@ -67,6 +69,7 @@ import com.sun.jersey.spi.container.servlet.ServletContainer;
/**
* @author Gunnar Hillert
* @author Biju Kunjummen
+ * @author Fahim Farook
*/
@Configuration
@Import(EurekaServerInitializerConfiguration.class)
@@ -169,9 +172,68 @@ public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {
@ConditionalOnMissingBean
public PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry,
ServerCodecs serverCodecs) {
- return new PeerEurekaNodes(registry, this.eurekaServerConfig,
+ return new RefreshablePeerEurekaNodes(registry, this.eurekaServerConfig,
this.eurekaClientConfig, serverCodecs, this.applicationInfoManager);
}
+
+ /**
+ * {@link PeerEurekaNodes} which updates peers when /refresh is invoked.
+ * Peers are updated only if
+ * eureka.client.use-dns-for-fetching-service-urls is
+ * false and one of following properties have changed.
+ *
+ *
+ * eureka.client.availability-zones
+ * eureka.client.region
+ * eureka.client.service-url.<zone>
+ *
+ */
+ static class RefreshablePeerEurekaNodes extends PeerEurekaNodes
+ implements ApplicationListener {
+
+ public RefreshablePeerEurekaNodes(
+ final PeerAwareInstanceRegistry registry,
+ final EurekaServerConfig serverConfig,
+ final EurekaClientConfig clientConfig,
+ final ServerCodecs serverCodecs,
+ final ApplicationInfoManager applicationInfoManager) {
+ super(registry, serverConfig, clientConfig, serverCodecs, applicationInfoManager);
+ }
+
+ @Override
+ public void onApplicationEvent(final EnvironmentChangeEvent event) {
+ if (shouldUpdate(event.getKeys())) {
+ updatePeerEurekaNodes(resolvePeerUrls());
+ }
+ }
+
+ /*
+ * Check whether specific properties have changed.
+ */
+ protected boolean shouldUpdate(final Set changedKeys) {
+ assert changedKeys != null;
+
+ // if eureka.client.use-dns-for-fetching-service-urls is true, then
+ // service-url will not be fetched from environment.
+ if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {
+ return false;
+ }
+
+ if (changedKeys.contains("eureka.client.region")) {
+ return true;
+ }
+
+ for (final String key : changedKeys) {
+ // property keys are not expected to be null.
+ if (key.startsWith("eureka.client.service-url.") ||
+ key.startsWith("eureka.client.availability-zones.")) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
@Bean
public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs,
diff --git a/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/RefreshablePeerEurekaNodesTests.java b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/RefreshablePeerEurekaNodesTests.java
new file mode 100644
index 00000000..dcf2ce85
--- /dev/null
+++ b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/RefreshablePeerEurekaNodesTests.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2013-2017 the original author or 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 org.springframework.cloud.netflix.eureka.server;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyListOf;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.util.EnvironmentTestUtils;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
+import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean;
+import org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration.RefreshablePeerEurekaNodes;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import com.netflix.appinfo.ApplicationInfoManager;
+import com.netflix.discovery.EurekaClientConfig;
+import com.netflix.eureka.EurekaServerConfig;
+import com.netflix.eureka.cluster.PeerEurekaNodes;
+import com.netflix.eureka.registry.PeerAwareInstanceRegistry;
+import com.netflix.eureka.resources.ServerCodecs;
+
+/**
+ * @author Fahim Farook
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+ classes = RefreshablePeerEurekaNodesTests.Application.class,
+ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+ value = {
+ "spring.application.name=eureka-server",
+ "eureka.client.service-url.defaultZone=http://localhost:8678/eureka/"
+ })
+public class RefreshablePeerEurekaNodesTests {
+
+ @Autowired
+ private ConfigurableApplicationContext context;
+
+ @Autowired
+ private PeerEurekaNodes peerEurekaNodes;
+
+ @Value("${local.server.port}")
+ private int port = 0;
+
+ private static final String DEFAULT_ZONE = "eureka.client.service-url.defaultZone";
+ private static final String REGION = "eureka.client.region";
+ private static final String USE_DNS = "eureka.client.use-dns-for-fetching-service-urls";
+
+ @Test
+ public void notUpdatedWhenDnsIsTrue() {
+ changeProperty(
+ "eureka.client.use-dns-for-fetching-service-urls=true",
+ "eureka.client.region=unavailable-region", // to force defaultZone
+ "eureka.client.service-url.defaultZone=http://default-host1:8678/eureka/");
+ this.context.publishEvent(new EnvironmentChangeEvent(new HashSet(Arrays.asList(USE_DNS, DEFAULT_ZONE))));
+
+ assertFalse("PeerEurekaNodes' are updated when eureka.client.use-dns-for-fetching-service-urls is true",
+ serviceUrlMatches("http://default-host1:8678/eureka/"));
+ }
+
+ @Test
+ public void updatedWhenDnsIsFalse() {
+ changeProperty(
+ "eureka.client.use-dns-for-fetching-service-urls=false",
+ "eureka.client.region=unavailable-region", // to force defaultZone
+ "eureka.client.service-url.defaultZone=http://default-host2:8678/eureka/");
+ this.context.publishEvent(new EnvironmentChangeEvent(new HashSet(Arrays.asList(USE_DNS, DEFAULT_ZONE))));
+
+ assertTrue("PeerEurekaNodes' are not updated when eureka.client.use-dns-for-fetching-service-urls is false",
+ serviceUrlMatches("http://default-host2:8678/eureka/"));
+ }
+
+
+ @Test
+ public void updatedWhenRegionChanged() {
+ changeProperty(
+ "eureka.client.use-dns-for-fetching-service-urls=false",
+ "eureka.client.region=region1",
+ "eureka.client.availability-zones.region1=region1-zone",
+ "eureka.client.availability-zones.region2=region2-zone",
+ "eureka.client.service-url.region1-zone=http://region1-zone-host:8678/eureka/",
+ "eureka.client.service-url.region2-zone=http://region2-zone-host:8678/eureka/");
+ this.context.publishEvent(new EnvironmentChangeEvent(Collections.singleton(REGION)));
+ assertTrue("PeerEurekaNodes' are not updated when eureka.client.region is changed",
+ serviceUrlMatches("http://region1-zone-host:8678/eureka/"));
+
+ changeProperty("eureka.client.region=region2");
+ this.context.publishEvent(new EnvironmentChangeEvent(Collections.singleton(REGION)));
+ assertTrue("PeerEurekaNodes' are not updated when eureka.client.region is changed",
+ serviceUrlMatches("http://region2-zone-host:8678/eureka/"));
+ }
+
+ @Test
+ public void updatedWhenAvailabilityZoneChanged() {
+ changeProperty(
+ "eureka.client.use-dns-for-fetching-service-urls=false",
+ "eureka.client.region=region4",
+ "eureka.client.availability-zones.region3=region3-zone",
+ "eureka.client.service-url.region4-zone=http://region4-zone-host:8678/eureka/",
+ "eureka.client.service-url.defaultZone=http://default-host3:8678/eureka/");
+ this.context.publishEvent(new EnvironmentChangeEvent(Collections.singleton("eureka.client.availability-zones.region3")));
+ assertTrue(this.peerEurekaNodes.getPeerEurekaNodes().get(0).getServiceUrl().equals("http://default-host3:8678/eureka/"));
+
+ changeProperty("eureka.client.availability-zones.region4=region4-zone");
+ this.context.publishEvent(new EnvironmentChangeEvent(Collections.singleton("eureka.client.availability-zones.region4")));
+ assertTrue("PeerEurekaNodes' are not updated when eureka.client.availability-zones are changed",
+ serviceUrlMatches("http://region4-zone-host:8678/eureka/"));
+ }
+
+ @Test
+ public void notUpdatedWhenIrrelevantPropertiesChanged() {
+ // Only way to test this is verifying whether updatePeerEurekaNodes() is invoked.
+
+ // PeerEurekaNodes.updatePeerEurekaNodes() is not public, hence cannot verify with Mockito.
+ class VerifyablePeerEurekNode extends RefreshablePeerEurekaNodes {
+ public VerifyablePeerEurekNode(PeerAwareInstanceRegistry registry, EurekaServerConfig serverConfig,
+ EurekaClientConfig clientConfig, ServerCodecs serverCodecs,
+ ApplicationInfoManager applicationInfoManager) {
+ super(registry, serverConfig, clientConfig, serverCodecs, applicationInfoManager);
+ }
+
+ protected void updatePeerEurekaNodes(List newPeerUrls) {
+ super.updatePeerEurekaNodes(newPeerUrls);
+ }
+ }
+
+ // Create stubs.
+ final EurekaClientConfigBean configClientBean = mock(EurekaClientConfigBean.class);
+ when(configClientBean.isUseDnsForFetchingServiceUrls()).thenReturn(false);
+ final VerifyablePeerEurekNode mock = spy(new VerifyablePeerEurekNode(null, null, configClientBean, null, null));
+
+ mock.onApplicationEvent(new EnvironmentChangeEvent(Collections.singleton("some.irrelevant.property")));
+ verify(mock, never()).updatePeerEurekaNodes(anyListOf(String.class));
+ }
+
+ @Test
+ public void peerEurekaNodesIsRefreshablePeerEurekaNodes() {
+ assertNotNull(this.peerEurekaNodes);
+ assertTrue("PeerEurekaNodes should be an instance of RefreshablePeerEurekaNodes",
+ this.peerEurekaNodes instanceof RefreshablePeerEurekaNodes);
+ }
+
+
+ @Test
+ public void serviceUrlsCountAsSoonAsRefreshed() {
+ changeProperty("eureka.client.service-url.defaultZone=http://defaul-host3:8678/eureka/,http://defaul-host4:8678/eureka/");
+ forceUpdate();
+ assertThat("PeerEurekaNodes' peer count is incorrect.",
+ this.peerEurekaNodes.getPeerEurekaNodes().size(), is(2));
+ }
+
+
+ @Test
+ public void serviceUrlsValueAsSoonAsRefreshed() {
+ changeProperty("eureka.client.service-url.defaultZone=http://defaul-host4:8678/eureka/");
+ forceUpdate();
+ assertTrue("PeerEurekaNodes' new peer[0] is incorrect",
+ serviceUrlMatches("http://defaul-host4:8678/eureka/"));
+ }
+
+ @Test
+ public void dashboardUpdatedAsSoonAsRefreshed() {
+ changeProperty("eureka.client.service-url.defaultZone=http://defaul-host5:8678/eureka/");
+ forceUpdate();
+ final ResponseEntity entity = new TestRestTemplate()
+ .getForEntity("http://localhost:" + this.port + "/", String.class);
+ assertEquals(HttpStatus.OK, entity.getStatusCode());
+ final String body = entity.getBody();
+ assertNotNull(body);
+ assertTrue("DS Replicas not updated in the Eureka Server dashboard",
+ body.contains("http://defaul-host5:8678/eureka/"));
+ }
+
+ @Test
+ public void notUpdatedForRelaxedKeys() {
+ changeProperty(
+ "eureka.client.use-dns-for-fetching-service-urls=false",
+ "eureka.client.region=unavailable-region", // to force defaultZone
+ "eureka.client.service-url.defaultZone=http://defaul-host6:8678/eureka/");
+ this.context.publishEvent(new EnvironmentChangeEvent(Collections.singleton("eureka.client.serviceUrl.defaultZone")));
+ assertFalse("PeerEurekaNodes' are updated for keys with relaxed binding",
+ serviceUrlMatches("http://defaul-host6:8678/eureka/"));
+ }
+
+ @EnableEurekaServer
+ @Configuration
+ @EnableAutoConfiguration(exclude = {
+ org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration.class,
+ org.springframework.boot.actuate.autoconfigure.ManagementWebSecurityAutoConfiguration.class })
+ protected static class Application {
+
+ public static void main(String[] args) {
+ new SpringApplicationBuilder(RefreshablePeerEurekaNodesTests.Application.class).run(args);
+ }
+ }
+
+ /*
+ * Changes the value of given key in the environment.
+ */
+ private void changeProperty(final String... pairs) {
+ EnvironmentTestUtils.addEnvironment(this.context, pairs);
+ }
+
+ /*
+ * Refreshes the context with properties satisfying to invoke update.
+ */
+ private void forceUpdate() {
+ changeProperty(
+ "eureka.client.use-dns-for-fetching-service-urls=false",
+ "eureka.client.region=unavailable-region"); // to force defaultZone
+ this.context.publishEvent(
+ new EnvironmentChangeEvent(Collections.singleton("eureka.client.service-url.defaultZone")));
+ }
+
+ /*
+ * Whether the first element in PeerEurekaNodes matches the given url.
+ */
+ private boolean serviceUrlMatches(final String serviceUrl) {
+ return this.peerEurekaNodes.getPeerEurekaNodes().get(0).getServiceUrl().equals(serviceUrl);
+ }
+}