diff --git a/docs/modules/ROOT/pages/spring-cloud-netflix.adoc b/docs/modules/ROOT/pages/spring-cloud-netflix.adoc index 31148a15a5..50b236bc36 100755 --- a/docs/modules/ROOT/pages/spring-cloud-netflix.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-netflix.adoc @@ -387,6 +387,12 @@ When a refresh occurs clients will be unregistered from the Eureka server and th where all instance of a given service are not available. One way to eliminate this from happening is to disable the ability to refresh Eureka clients. To do this set `eureka.client.refresh.enable=false`. +=== Pausing Eureka Clients + +If the `pause` and `resume` actuator endpoints from Spring Cloud Commons are enabled, calling `/actuator/pause` sets +the instance's status to `OUT_OF_SERVICE` in the Eureka server, which takes it out of rotation for other clients. +Calling `/actuator/resume` removes that override, and the instance shows its own status again after its next heartbeat. + === Using Eureka with Spring Cloud LoadBalancer We offer support for the Spring Cloud LoadBalancer `ZonePreferenceServiceInstanceListSupplier`. diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java index 33efb49cf4..c694000c60 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java @@ -75,11 +75,13 @@ import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationProperties; import org.springframework.cloud.client.serviceregistry.ServiceRegistryAutoConfiguration; import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.cloud.context.restart.PauseHandler; import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.cloud.netflix.eureka.metadata.DefaultManagementMetadataProvider; import org.springframework.cloud.netflix.eureka.metadata.ManagementMetadata; import org.springframework.cloud.netflix.eureka.metadata.ManagementMetadataProvider; import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaAutoServiceRegistration; +import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaPauseHandler; import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration; import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry; import org.springframework.cloud.util.ProxyUtils; @@ -251,6 +253,21 @@ public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(ApplicationCo return new EurekaAutoServiceRegistration(context, registry, registration); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(PauseHandler.class) + @ConditionalOnProperty(value = "eureka.client.register-with-eureka", matchIfMissing = true) + protected static class EurekaPauseHandlerConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnBean(AutoServiceRegistrationProperties.class) + @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true) + public EurekaPauseHandler eurekaPauseHandler(EurekaServiceRegistry registry, EurekaRegistration registration) { + return new EurekaPauseHandler(registry, registration); + } + + } + @Configuration(proxyBeanMethods = false) @ConditionalOnMissingRefreshScope protected static class EurekaClientConfiguration { diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaPauseHandler.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaPauseHandler.java new file mode 100644 index 0000000000..afc8e60ccc --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaPauseHandler.java @@ -0,0 +1,50 @@ +/* + * Copyright 2013-present 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 + * + * https://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.serviceregistry; + +import com.netflix.appinfo.InstanceInfo.InstanceStatus; + +import org.springframework.cloud.context.restart.PauseHandler; + +/** + * {@link PauseHandler} that takes the instance out of service in Eureka when the + * application is paused and cancels that status override when it is resumed. + * + * @author Akhil CH + */ +public class EurekaPauseHandler implements PauseHandler { + + private final EurekaServiceRegistry serviceRegistry; + + private final EurekaRegistration registration; + + public EurekaPauseHandler(EurekaServiceRegistry serviceRegistry, EurekaRegistration registration) { + this.serviceRegistry = serviceRegistry; + this.registration = registration; + } + + @Override + public void pause() { + this.serviceRegistry.setStatus(this.registration, InstanceStatus.OUT_OF_SERVICE.name()); + } + + @Override + public void resume() { + this.serviceRegistry.setStatus(this.registration, "CANCEL_OVERRIDE"); + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfigurationTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfigurationTests.java index b629f7b52d..cb5d26a4f2 100644 --- a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfigurationTests.java +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfigurationTests.java @@ -40,6 +40,7 @@ import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; @@ -49,8 +50,10 @@ import org.springframework.cloud.commons.util.UtilAutoConfiguration; import org.springframework.cloud.context.config.ContextRefreshedWithApplicationEvent; import org.springframework.cloud.context.refresh.ContextRefresher; +import org.springframework.cloud.context.restart.PauseHandler; import org.springframework.cloud.context.scope.GenericScope; import org.springframework.cloud.netflix.eureka.config.DiscoveryClientOptionalArgsConfiguration; +import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaPauseHandler; import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; @@ -650,6 +653,37 @@ void shouldNotHaveDiscoveryClientWhenBlockingDiscoveryDisabled() { }); } + @Test + void shouldHaveEurekaPauseHandler() { + pauseHandlerContextRunner().run(context -> assertThat(context).hasSingleBean(EurekaPauseHandler.class)); + } + + @Test + void shouldNotHaveEurekaPauseHandlerWhenAutoRegistrationDisabled() { + pauseHandlerContextRunner().withPropertyValues("spring.cloud.service-registry.auto-registration.enabled=false") + .run(context -> assertThat(context).doesNotHaveBean(EurekaPauseHandler.class)); + } + + @Test + void shouldNotHaveEurekaPauseHandlerWhenRegisterWithEurekaDisabled() { + pauseHandlerContextRunner().withPropertyValues("eureka.client.register-with-eureka=false") + .run(context -> assertThat(context).doesNotHaveBean(EurekaPauseHandler.class)); + } + + @Test + void shouldNotHaveEurekaPauseHandlerWhenPauseHandlerIsNotOnClasspath() { + pauseHandlerContextRunner().withClassLoader(new FilteredClassLoader(PauseHandler.class)) + .run(context -> assertThat(context).doesNotHaveBean(EurekaPauseHandler.class)); + } + + private ApplicationContextRunner pauseHandlerContextRunner() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(UtilAutoConfiguration.class, + DiscoveryClientOptionalArgsConfiguration.class, EurekaClientAutoConfiguration.class)) + .withUserConfiguration(AutoServiceRegistrationConfiguration.class) + .withPropertyValues("eureka.client.fetch-registry=false"); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) private void assertBeanNotPresent(Class beanClass) { try { diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaPauseHandlerTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaPauseHandlerTests.java new file mode 100644 index 0000000000..39d0a9f6d2 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaPauseHandlerTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013-present 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 + * + * https://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.serviceregistry; + +import com.netflix.appinfo.ApplicationInfoManager; +import com.netflix.appinfo.InstanceInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.cloud.commons.util.InetUtilsProperties; +import org.springframework.cloud.netflix.eureka.CloudEurekaClient; +import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; +import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean; +import org.springframework.context.ApplicationEventPublisher; + +import static com.netflix.appinfo.InstanceInfo.InstanceStatus.OUT_OF_SERVICE; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +/** + * @author Akhil CH + */ +class EurekaPauseHandlerTests { + + private final CloudEurekaClient eurekaClient = mock(CloudEurekaClient.class); + + private final InstanceInfo instanceInfo = mock(InstanceInfo.class); + + private EurekaPauseHandler pauseHandler; + + @BeforeEach + void setup() { + ApplicationInfoManager applicationInfoManager = mock(ApplicationInfoManager.class); + when(applicationInfoManager.getInfo()).thenReturn(this.instanceInfo); + + EurekaRegistration registration = EurekaRegistration + .builder(new EurekaInstanceConfigBean(new InetUtils(new InetUtilsProperties()))) + .with(this.eurekaClient) + .with(applicationInfoManager) + .with(new EurekaClientConfigBean(), mock(ApplicationEventPublisher.class)) + .build(); + + this.pauseHandler = new EurekaPauseHandler(new EurekaServiceRegistry(), registration); + } + + @Test + void pauseSetsInstanceOutOfService() { + this.pauseHandler.pause(); + + verify(this.eurekaClient).setStatus(OUT_OF_SERVICE, this.instanceInfo); + verifyNoMoreInteractions(this.eurekaClient); + } + + @Test + void resumeCancelsStatusOverride() { + this.pauseHandler.resume(); + + verify(this.eurekaClient).cancelOverrideStatus(this.instanceInfo); + verifyNoMoreInteractions(this.eurekaClient); + } + +}