服务预热

pull/1800/head
shedfreewu 2 weeks ago
parent e03dcf2a03
commit 96f16231cd

@ -62,5 +62,11 @@
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-slf4j</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

@ -19,27 +19,33 @@ package com.tencent.cloud.polaris.eager.config;
import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClient;
import com.tencent.cloud.polaris.discovery.reactive.PolarisReactiveDiscoveryClient;
import com.tencent.cloud.polaris.eager.instrument.feign.FeignEagerLoadSmartLifecycle;
import com.tencent.cloud.polaris.eager.instrument.feign.FeignEagerLoadContextInitializer;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.LoadBalancerEagerContextInitializer;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.LoadBalancerEagerLoadProperties;
import com.tencent.cloud.polaris.eager.instrument.services.ServicesEagerLoadSmartLifecycle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(LoadBalancerEagerLoadProperties.class)
@ConditionalOnProperty(name = "spring.cloud.polaris.discovery.eager-load.enabled", havingValue = "true", matchIfMissing = true)
public class PolarisEagerLoadAutoConfiguration {
@Bean
@ConditionalOnClass(name = "feign.Feign")
@ConditionalOnProperty(name = "spring.cloud.polaris.discovery.eager-load.feign.enabled", havingValue = "true", matchIfMissing = true)
public FeignEagerLoadSmartLifecycle feignEagerLoadSmartLifecycle(
ApplicationContext applicationContext, @Autowired(required = false) PolarisDiscoveryClient polarisDiscoveryClient,
@Autowired(required = false) PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient) {
return new FeignEagerLoadSmartLifecycle(applicationContext, polarisDiscoveryClient, polarisReactiveDiscoveryClient);
public FeignEagerLoadContextInitializer feignEagerLoadContextInitializer(
ApplicationContext applicationContext,
LoadBalancerClientFactory loadBalancerClientFactory,
LoadBalancerEagerLoadProperties loadBalancerEagerLoadProperties) {
return new FeignEagerLoadContextInitializer(applicationContext, loadBalancerClientFactory, loadBalancerEagerLoadProperties);
}
@Bean
@ConditionalOnProperty(name = "spring.cloud.polaris.discovery.eager-load.services.enabled", havingValue = "true", matchIfMissing = true)
@ -48,5 +54,12 @@ public class PolarisEagerLoadAutoConfiguration {
@Autowired(required = false) PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient) {
return new ServicesEagerLoadSmartLifecycle(polarisDiscoveryClient, polarisReactiveDiscoveryClient);
}
@Bean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.eager-load.enabled", matchIfMissing = true)
public LoadBalancerEagerContextInitializer loadBalancerEagerContextInitializer(
LoadBalancerClientFactory loadBalancerClientFactory, LoadBalancerEagerLoadProperties properties) {
return new LoadBalancerEagerContextInitializer(loadBalancerClientFactory, properties.getClients());
}
}

@ -20,9 +20,11 @@ package com.tencent.cloud.polaris.eager.instrument.feign;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.net.URI;
import java.util.HashSet;
import java.util.Set;
import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClient;
import com.tencent.cloud.polaris.discovery.reactive.PolarisReactiveDiscoveryClient;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.LoadBalancerEagerLoadProperties;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.LoadBalancerWarmUpUtils;
import com.tencent.polaris.api.utils.StringUtils;
import feign.Target;
import org.slf4j.Logger;
@ -30,30 +32,48 @@ import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.AopProxy;
import org.springframework.aop.framework.JdkDynamicAopProxyUtils;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.ApplicationListener;
public class FeignEagerLoadSmartLifecycle implements SmartLifecycle {
/**
* Feign eager load context initializer.
* Implements ApplicationListener<ApplicationReadyEvent> to warm up FeignClient services
* after the application is ready.
*
* @author Yuwei Fu
*/
public class FeignEagerLoadContextInitializer implements ApplicationListener<ApplicationReadyEvent> {
private static final Logger LOG = LoggerFactory.getLogger(FeignEagerLoadSmartLifecycle.class);
private static final Logger LOG = LoggerFactory.getLogger(FeignEagerLoadContextInitializer.class);
private final ApplicationContext applicationContext;
private ApplicationContext applicationContext;
private final PolarisDiscoveryClient polarisDiscoveryClient;
private LoadBalancerClientFactory loadBalancerClientFactory;
private final PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient;
private LoadBalancerEagerLoadProperties loadBalancerEagerLoadProperties;
public FeignEagerLoadSmartLifecycle(ApplicationContext applicationContext, PolarisDiscoveryClient polarisDiscoveryClient,
PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient) {
public FeignEagerLoadContextInitializer(ApplicationContext applicationContext,
LoadBalancerClientFactory loadBalancerClientFactory,
LoadBalancerEagerLoadProperties loadBalancerEagerLoadProperties) {
this.applicationContext = applicationContext;
this.polarisDiscoveryClient = polarisDiscoveryClient;
this.polarisReactiveDiscoveryClient = polarisReactiveDiscoveryClient;
this.loadBalancerClientFactory = loadBalancerClientFactory;
this.loadBalancerEagerLoadProperties = loadBalancerEagerLoadProperties;
}
@Override
public void start() {
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
LOG.info("feign eager-load start");
// Get services that are already warmed by LoadBalancerEagerContextInitializer
Set<String> skipServices = getLoadBalancerEagerLoadServices();
// Set to track already warmed services
Set<String> warmedServices = new HashSet<>();
// Warm up FeignClient services
for (Object bean : applicationContext.getBeansWithAnnotation(FeignClient.class).values()) {
try {
if (Proxy.isProxyClass(bean.getClass())) {
@ -70,17 +90,22 @@ public class FeignEagerLoadSmartLifecycle implements SmartLifecycle {
}
String serviceName = URI.create(url).getHost();
LOG.info("[{}] eager-load start, feign name: {}", serviceName, hardCodedTarget.name());
if (polarisDiscoveryClient != null) {
polarisDiscoveryClient.getInstances(serviceName);
// Skip if already warmed by LoadBalancerEagerContextInitializer
if (skipServices.contains(serviceName)) {
LOG.debug("[{}] skip eager-load, already configured in LoadBalancerEagerLoadProperties.clients", serviceName);
continue;
}
else if (polarisReactiveDiscoveryClient != null) {
polarisReactiveDiscoveryClient.getInstances(serviceName).subscribe();
// Skip if already warmed in this round
if (warmedServices.contains(serviceName)) {
LOG.debug("[{}] already warmed, skip.", serviceName);
continue;
}
else {
LOG.warn("[{}] no discovery client found.", serviceName);
}
LOG.info("[{}] eager-load end", serviceName);
LOG.info("[{}] eager-load start, feign name: {}", serviceName, hardCodedTarget.name());
LoadBalancerWarmUpUtils.warmUp(loadBalancerClientFactory, serviceName);
warmedServices.add(serviceName);
}
}
}
@ -90,14 +115,28 @@ public class FeignEagerLoadSmartLifecycle implements SmartLifecycle {
}
}
LOG.info("feign eager-load end");
}
/**
* Get services configured in LoadBalancerEagerLoadProperties.
* These services are warmed by LoadBalancerEagerContextInitializer.
* @return set of service names to skip
*/
private Set<String> getLoadBalancerEagerLoadServices() {
Set<String> services = new HashSet<>();
if (loadBalancerEagerLoadProperties != null
&& loadBalancerEagerLoadProperties.isEnabled()
&& loadBalancerEagerLoadProperties.getClients() != null) {
services.addAll(loadBalancerEagerLoadProperties.getClients());
}
return services;
}
public static Target.HardCodedTarget<?> getHardCodedTarget(Object proxy) {
try {
int count = 0;
Object invocationHandler = proxy;
// 避免死循环
// Avoid infinite loop
while (count++ < 100) {
invocationHandler = Proxy.getInvocationHandler(invocationHandler);
if (invocationHandler instanceof AopProxy) {
@ -122,19 +161,4 @@ public class FeignEagerLoadSmartLifecycle implements SmartLifecycle {
}
return null;
}
@Override
public void stop() {
}
@Override
public boolean isRunning() {
return false;
}
@Override
public int getPhase() {
return 10;
}
}

@ -0,0 +1,48 @@
package com.tencent.cloud.polaris.eager.instrument.loadbalancer;
import java.util.List;
import com.tencent.polaris.api.utils.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.ApplicationListener;
/**
* @author Yuwei Fu
*/
public class LoadBalancerEagerContextInitializer implements ApplicationListener<ApplicationReadyEvent> {
private static final Logger LOG = LoggerFactory.getLogger(LoadBalancerEagerContextInitializer.class);
private final LoadBalancerClientFactory factory;
private final List<String> serviceNames;
public LoadBalancerEagerContextInitializer(LoadBalancerClientFactory factory, List<String> serviceNames) {
this.factory = factory;
this.serviceNames = serviceNames;
}
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
LOG.info("spring cloud eager-load start");
try {
if (!CollectionUtils.isEmpty(serviceNames)) {
for (String serviceName : serviceNames) {
LoadBalancerWarmUpUtils.warmUp(factory, serviceName);
}
}
LOG.info("spring cloud eager-load end");
} catch (Exception e) {
if (LOG.isDebugEnabled()) {
LOG.debug("spring cloud eager-load failed.", e);
}
}
}
}

@ -0,0 +1,30 @@
package com.tencent.cloud.polaris.eager.instrument.loadbalancer;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("spring.cloud.loadbalancer.eager-load")
public class LoadBalancerEagerLoadProperties {
private List<String> clients;
private boolean enabled = true;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public List<String> getClients() {
return clients;
}
public void setClients(List<String> clients) {
this.clients = clients;
}
}

@ -0,0 +1,64 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.eager.instrument.loadbalancer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
/**
* Utility class for load balancer warm-up operations.
* Provides common warm-up logic for eager loading services.
*
* @author Yuwei Fu
*/
public final class LoadBalancerWarmUpUtils {
private static final Logger LOG = LoggerFactory.getLogger(LoadBalancerWarmUpUtils.class);
private LoadBalancerWarmUpUtils() {
}
/**
* Warm up a service by triggering load balancer initialization.
* @param factory the LoadBalancerClientFactory
* @param serviceName the service name to warm up
* @return true if warm-up succeeded, false otherwise
*/
public static boolean warmUp(LoadBalancerClientFactory factory, String serviceName) {
try {
ReactiveLoadBalancer<ServiceInstance> loadBalancer = factory.getInstance(serviceName);
if (loadBalancer != null) {
loadBalancer.choose();
LOG.info("[{}] eager-load end", serviceName);
return true;
}
else {
LOG.warn("[{}] no loadBalancer found.", serviceName);
return false;
}
}
catch (Exception e) {
LOG.debug("[{}] eager-load failed.", serviceName, e);
return false;
}
}
}

@ -0,0 +1,249 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.eager.instrument.feign;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.LoadBalancerEagerLoadProperties;
import com.tencent.cloud.polaris.registry.PolarisAutoServiceRegistration;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
/**
* Test for {@link FeignEagerLoadContextInitializer}.
*
* @author Test Author
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = DEFINED_PORT,
classes = FeignEagerLoadContextInitializerTest.TestApplication.class,
properties = {
"server.port=48085",
"spring.config.location = classpath:application-test.yml",
"spring.main.web-application-type = servlet",
"spring.cloud.gateway.enabled = false",
"spring.cloud.polaris.discovery.eager-load.enabled = true",
"spring.cloud.polaris.discovery.eager-load.feign.enabled = true",
"spring.cloud.loadbalancer.eager-load.enabled = true",
"spring.cloud.loadbalancer.eager-load.clients = test-service-1,test-service-2,test-feign-service-skip"
})
public class FeignEagerLoadContextInitializerTest {
@MockBean
private LoadBalancerClientFactory loadBalancerClientFactory;
@Autowired
private FeignEagerLoadContextInitializer feignEagerLoadContextInitializer;
@Autowired
private LoadBalancerEagerLoadProperties loadBalancerEagerLoadProperties;
@Autowired
private ConfigurableApplicationContext applicationContext;
@BeforeEach
public void setUp() {
reset(loadBalancerClientFactory);
}
@Test
public void testLoadBalancerEagerLoadPropertiesLoaded() {
// Verify LoadBalancerEagerLoadProperties is loaded correctly
assertThat(loadBalancerEagerLoadProperties.getClients())
.isNotNull()
.containsExactlyInAnyOrder("test-service-1", "test-service-2", "test-feign-service-skip");
assertThat(loadBalancerEagerLoadProperties.isEnabled()).isTrue();
}
@Test
public void testFeignClient() {
// Prepare mock
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance(anyString())).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
feignEagerLoadContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify services in LoadBalancerEagerLoadProperties.clients are NOT warmed (skipped)
// because they are handled by LoadBalancerEagerContextInitializer
verify(loadBalancerClientFactory, never()).getInstance("test-service-1");
verify(loadBalancerClientFactory, never()).getInstance("test-service-2");
// Verify FeignClient services are warmed (without url)
verify(loadBalancerClientFactory, times(1)).getInstance("test-feign-service");
verify(loadBalancerClientFactory, times(1)).getInstance("test-feign-service-http");
verify(loadBalancerClientFactory, times(1)).getInstance("test-feign-service-https");
// Verify FeignClient with url is NOT warmed
verify(loadBalancerClientFactory, never()).getInstance("localhost:48085");
// Verify choose method is called
verify(mockLoadBalancer, times(3)).choose();
}
@Test
public void testSkipServicesInLoadBalancerEagerLoadProperties() {
// Prepare mock
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance(anyString())).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
feignEagerLoadContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify services configured in LoadBalancerEagerLoadProperties.clients are skipped
// test-feign-service-skip is configured in LoadBalancerEagerLoadProperties.clients
verify(loadBalancerClientFactory, never()).getInstance("test-feign-service-skip");
}
@Test
public void testFeignClientWithHttpPrefix() {
// Prepare mock
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance(anyString())).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
feignEagerLoadContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify service name is correctly extracted from http:// prefix
verify(loadBalancerClientFactory, times(1)).getInstance("test-feign-service-http");
}
@Test
public void testFeignClientWithHttpsPrefix() {
// Prepare mock
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance(anyString())).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
feignEagerLoadContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify service name is correctly extracted from https:// prefix
verify(loadBalancerClientFactory, times(1)).getInstance("test-feign-service-https");
}
@SpringBootApplication
@EnableFeignClients
@RestController
protected static class TestApplication {
@Bean
public TestBeanPostProcessor testBeanPostProcessor() {
return new TestBeanPostProcessor();
}
@Bean
public FeignAspect feignAspect() {
return new FeignAspect();
}
@RequestMapping("/test")
public String test() {
return "test";
}
// Normal FeignClient (without url)
@FeignClient(name = "test-feign-service")
public interface TestFeignClient {
@RequestMapping("/test")
String test();
}
// FeignClient with http:// prefix
@FeignClient(name = "http://test-feign-service-http")
public interface TestFeignClientWithHttp {
@RequestMapping("/test")
String test();
}
// FeignClient with https:// prefix
@FeignClient(name = "https://test-feign-service-https")
public interface TestFeignClientWithHttps {
@RequestMapping("/test")
String test();
}
// FeignClient with url (should skip warm-up)
@FeignClient(name = "test-feign-with-url", url = "http://localhost:48085")
public interface TestFeignClientWithUrl {
@RequestMapping("/test")
String test();
}
// FeignClient that is also in LoadBalancerEagerLoadProperties.clients
// This service should be skipped in FeignEagerLoadContextInitializer
@FeignClient(name = "test-feign-service-skip")
public interface TestFeignClientSkip {
@RequestMapping("/test")
String test();
}
}
static class TestBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof PolarisAutoServiceRegistration) {
return org.mockito.Mockito.mock(PolarisAutoServiceRegistration.class);
}
return bean;
}
}
@Aspect
static class FeignAspect {
private static final Logger LOG = LoggerFactory.getLogger(FeignAspect.class);
@Around("@within(org.springframework.cloud.openfeign.FeignClient)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
LOG.info("FeignAspect around");
return joinPoint.proceed();
}
}
}

@ -0,0 +1,164 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.eager.instrument.loadbalancer;
import com.tencent.cloud.polaris.registry.PolarisAutoServiceRegistration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
/**
* Test for {@link LoadBalancerEagerContextInitializer}.
*
* @author Test Author
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = DEFINED_PORT,
classes = LoadBalancerEagerContextInitializerTest.TestApplication.class,
properties = {
"server.port=48086",
"spring.config.location = classpath:application-test.yml",
"spring.main.web-application-type = servlet",
"spring.cloud.gateway.enabled = false",
"spring.cloud.polaris.discovery.eager-load.enabled = true",
"spring.cloud.loadbalancer.eager-load.enabled = true",
"spring.cloud.loadbalancer.eager-load.clients = test-service-1,test-service-2,test-service-3"
})
public class LoadBalancerEagerContextInitializerTest {
@MockBean
private LoadBalancerClientFactory loadBalancerClientFactory;
@Autowired
private LoadBalancerEagerContextInitializer loadBalancerEagerContextInitializer;
@Autowired
private LoadBalancerEagerLoadProperties loadBalancerEagerLoadProperties;
@BeforeEach
public void setUp() {
reset(loadBalancerClientFactory);
}
@Test
public void testLoadBalancerEagerLoadPropertiesLoaded() {
// Verify LoadBalancerEagerLoadProperties is loaded correctly
assertThat(loadBalancerEagerLoadProperties.getClients())
.isNotNull()
.containsExactlyInAnyOrder("test-service-1", "test-service-2", "test-service-3");
assertThat(loadBalancerEagerLoadProperties.isEnabled()).isTrue();
}
@Test
public void testWarmUpServices() {
// Prepare mock
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance(anyString())).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
loadBalancerEagerContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify all services in LoadBalancerEagerLoadProperties.clients are warmed
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-1");
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-2");
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-3");
// Verify choose method is called for each service
verify(mockLoadBalancer, times(3)).choose();
}
@Test
public void testWarmUpWithNullLoadBalancer() {
// Prepare mock - return null for some services
when(loadBalancerClientFactory.getInstance("test-service-1")).thenReturn(null);
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance("test-service-2")).thenReturn(mockLoadBalancer);
when(loadBalancerClientFactory.getInstance("test-service-3")).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
loadBalancerEagerContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify all services are attempted
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-1");
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-2");
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-3");
// Verify choose method is only called for non-null load balancers
verify(mockLoadBalancer, times(2)).choose();
}
@Test
public void testWarmUpWithException() {
// Prepare mock - throw exception for some services
when(loadBalancerClientFactory.getInstance("test-service-1"))
.thenThrow(new RuntimeException("Test exception"));
ReactiveLoadBalancer<ServiceInstance> mockLoadBalancer = mock(ReactiveLoadBalancer.class);
when(loadBalancerClientFactory.getInstance("test-service-2")).thenReturn(mockLoadBalancer);
when(loadBalancerClientFactory.getInstance("test-service-3")).thenReturn(mockLoadBalancer);
// Execute warm-up by triggering ApplicationReadyEvent
loadBalancerEagerContextInitializer.onApplicationEvent(mock(ApplicationReadyEvent.class));
// Verify all services are attempted (exception should not stop the loop)
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-1");
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-2");
verify(loadBalancerClientFactory, times(1)).getInstance("test-service-3");
}
@SpringBootApplication
protected static class TestApplication {
@Bean
public TestBeanPostProcessor testBeanPostProcessor() {
return new TestBeanPostProcessor();
}
}
static class TestBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof PolarisAutoServiceRegistration) {
return org.mockito.Mockito.mock(PolarisAutoServiceRegistration.class);
}
return bean;
}
}
}

@ -17,10 +17,12 @@
package com.tencent.cloud.plugin.unit.config;
import com.tencent.cloud.plugin.unit.discovery.UnitFeignEagerLoadSmartLifecycle;
import com.tencent.cloud.plugin.unit.discovery.UnitFeignEagerLoadContextInitializer;
import com.tencent.cloud.plugin.unit.discovery.UnitLoadBalancerEagerContextInitializer;
import com.tencent.cloud.plugin.unit.discovery.UnitPolarisDiscoveryClient;
import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClient;
import com.tencent.cloud.polaris.eager.instrument.feign.FeignEagerLoadSmartLifecycle;
import com.tencent.cloud.polaris.eager.instrument.feign.FeignEagerLoadContextInitializer;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.LoadBalancerEagerContextInitializer;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
@ -35,8 +37,12 @@ public class UnitBeanPostProcessor implements BeanPostProcessor {
return new UnitPolarisDiscoveryClient(discoveryClient);
}
if (bean instanceof FeignEagerLoadSmartLifecycle) {
return new UnitFeignEagerLoadSmartLifecycle();
if (bean instanceof FeignEagerLoadContextInitializer) {
return new UnitFeignEagerLoadContextInitializer();
}
if (bean instanceof LoadBalancerEagerContextInitializer) {
return new UnitLoadBalancerEagerContextInitializer();
}
return bean;

@ -17,21 +17,27 @@
package com.tencent.cloud.plugin.unit.discovery;
import com.tencent.cloud.polaris.eager.instrument.feign.FeignEagerLoadSmartLifecycle;
import com.tencent.cloud.polaris.eager.instrument.feign.FeignEagerLoadContextInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UnitFeignEagerLoadSmartLifecycle extends FeignEagerLoadSmartLifecycle {
import org.springframework.boot.context.event.ApplicationReadyEvent;
private static final Logger LOG = LoggerFactory.getLogger(UnitFeignEagerLoadSmartLifecycle.class);
/**
* Unit Feign eager load context initializer.
* Ignores feign eager load in unit mode.
*/
public class UnitFeignEagerLoadContextInitializer extends FeignEagerLoadContextInitializer {
private static final Logger LOG = LoggerFactory.getLogger(UnitFeignEagerLoadContextInitializer.class);
public UnitFeignEagerLoadSmartLifecycle() {
public UnitFeignEagerLoadContextInitializer() {
super(null, null, null);
}
@Override
public void start() {
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
LOG.info("ignore feign eager load in unit mode");
}
}

@ -0,0 +1,42 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.plugin.unit.discovery;
import com.tencent.cloud.polaris.eager.instrument.loadbalancer.LoadBalancerEagerContextInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
/**
* Unit LoadBalancer eager load context initializer.
* Ignores loadbalancer eager load in unit mode.
*/
public class UnitLoadBalancerEagerContextInitializer extends LoadBalancerEagerContextInitializer {
private static final Logger LOG = LoggerFactory.getLogger(UnitLoadBalancerEagerContextInitializer.class);
public UnitLoadBalancerEagerContextInitializer() {
super(null, null);
}
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
LOG.info("ignore loadbalancer eager load in unit mode");
}
}
Loading…
Cancel
Save