diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/startup/StartupOptimizationEndpointAutoConfiguration.java b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/startup/StartupOptimizationEndpointAutoConfiguration.java new file mode 100644 index 000000000..feef86036 --- /dev/null +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/java/com/alipay/sofa/boot/actuator/autoconfigure/startup/StartupOptimizationEndpointAutoConfiguration.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.alipay.sofa.boot.actuator.autoconfigure.startup; + +import com.alipay.sofa.boot.actuator.startup.StartupOptimizationEndpoint; +import com.alipay.sofa.boot.startup.StartupOptimizer; +import com.alipay.sofa.boot.startup.StartupReporter; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link StartupOptimizationEndpoint}. + * + * @author OpenAI + */ +@AutoConfiguration +@ConditionalOnBean(StartupReporter.class) +@ConditionalOnAvailableEndpoint(endpoint = StartupOptimizationEndpoint.class) +public class StartupOptimizationEndpointAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public StartupOptimizer startupOptimizer(StartupReporter startupReporter) { + return new StartupOptimizer(startupReporter); + } + + @Bean + @ConditionalOnMissingBean + public StartupOptimizationEndpoint startupOptimizationEndpoint(StartupOptimizer startupOptimizer, + ApplicationContext applicationContext) { + return new StartupOptimizationEndpoint(startupOptimizer, applicationContext); + } +} diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 6dbe5466a..71d449ba6 100644 --- a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -6,6 +6,7 @@ com.alipay.sofa.boot.actuator.autoconfigure.health.ReadinessEndpointAutoConfigur com.alipay.sofa.boot.actuator.autoconfigure.health.ReadinessRuntimeAutoConfiguration com.alipay.sofa.boot.actuator.autoconfigure.health.ManualReadinessCallbackEndpointAutoConfiguration com.alipay.sofa.boot.actuator.autoconfigure.startup.StartupEndPointAutoConfiguration +com.alipay.sofa.boot.actuator.autoconfigure.startup.StartupOptimizationEndpointAutoConfiguration com.alipay.sofa.boot.actuator.autoconfigure.rpc.RpcActuatorAutoConfiguration com.alipay.sofa.boot.actuator.autoconfigure.isle.IsleEndpointAutoConfiguration -com.alipay.sofa.boot.actuator.autoconfigure.threadpool.ThreadPoolEndpointAutoConfiguration \ No newline at end of file +com.alipay.sofa.boot.actuator.autoconfigure.threadpool.ThreadPoolEndpointAutoConfiguration diff --git a/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/startup/StartupOptimizationEndpointAutoConfigurationTests.java b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/startup/StartupOptimizationEndpointAutoConfigurationTests.java new file mode 100644 index 000000000..2538c11a6 --- /dev/null +++ b/sofa-boot-project/sofa-boot-actuator-autoconfigure/src/test/java/com/alipay/sofa/boot/actuator/autoconfigure/startup/StartupOptimizationEndpointAutoConfigurationTests.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.alipay.sofa.boot.actuator.autoconfigure.startup; + +import com.alipay.sofa.boot.actuator.startup.StartupOptimizationEndpoint; +import com.alipay.sofa.boot.isle.ApplicationRuntimeModel; +import com.alipay.sofa.boot.startup.StartupOptimizer; +import com.alipay.sofa.boot.startup.StartupReporter; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StartupOptimizationEndpointAutoConfiguration}. + * + * @author OpenAI + */ +public class StartupOptimizationEndpointAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations + .of(StartupOptimizationEndpointAutoConfiguration.class)) + .withClassLoader( + new FilteredClassLoader( + ApplicationRuntimeModel.class)); + + @Test + void runShouldHaveEndpointBean() { + this.contextRunner + .withBean(StartupReporter.class) + .withPropertyValues("management.endpoints.web.exposure.include=startup-optimization") + .run((context) -> assertThat(context) + .hasSingleBean(StartupOptimizer.class) + .hasSingleBean(StartupOptimizationEndpoint.class)); + } + + @Test + void runWhenNotStartupReporterBeanShouldNotHaveEndpointBean() { + this.contextRunner + .withPropertyValues("management.endpoints.web.exposure.include=startup-optimization") + .run((context) -> assertThat(context) + .doesNotHaveBean(StartupOptimizer.class) + .doesNotHaveBean(StartupOptimizationEndpoint.class)); + } + + @Test + void runWhenNotExposedShouldNotHaveEndpointBean() { + this.contextRunner + .withBean(StartupReporter.class) + .run((context) -> assertThat(context) + .doesNotHaveBean(StartupOptimizationEndpoint.class)); + } +} diff --git a/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/startup/StartupOptimizationEndpoint.java b/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/startup/StartupOptimizationEndpoint.java new file mode 100644 index 000000000..2c9ada919 --- /dev/null +++ b/sofa-boot-project/sofa-boot-actuator/src/main/java/com/alipay/sofa/boot/actuator/startup/StartupOptimizationEndpoint.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.alipay.sofa.boot.actuator.startup; + +import com.alipay.sofa.boot.startup.BeanInitInfo; +import com.alipay.sofa.boot.startup.StartupOptimizer; +import com.alipay.sofa.boot.startup.StartupReport; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.context.ApplicationContext; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import java.util.List; + +/** + * {@link Endpoint @Endpoint} to expose startup optimization analysis. + * + * @author OpenAI + */ +@Endpoint(id = "startup-optimization") +public class StartupOptimizationEndpoint { + + private static final String SLOW_BEANS_OPERATION = "slow-beans"; + + private final StartupOptimizer optimizer; + + private final ApplicationContext applicationContext; + + public StartupOptimizationEndpoint(StartupOptimizer optimizer, + ApplicationContext applicationContext) { + this.optimizer = optimizer; + this.applicationContext = applicationContext; + } + + @ReadOperation + public StartupReport analyze() { + return optimizer.analyzeStartupBottlenecks(applicationContext); + } + + @ReadOperation + public List slowBeans(@Selector String operation, @Nullable Integer top) { + Assert.isTrue(SLOW_BEANS_OPERATION.equals(operation), + "Only slow-beans operation is supported"); + return optimizer.findSlowBeans(applicationContext, top != null ? top : 10); + } +} diff --git a/sofa-boot-project/sofa-boot-actuator/src/test/java/com/alipay/sofa/boot/actuator/startup/StartupOptimizationEndpointTests.java b/sofa-boot-project/sofa-boot-actuator/src/test/java/com/alipay/sofa/boot/actuator/startup/StartupOptimizationEndpointTests.java new file mode 100644 index 000000000..8da8b794a --- /dev/null +++ b/sofa-boot-project/sofa-boot-actuator/src/test/java/com/alipay/sofa/boot/actuator/startup/StartupOptimizationEndpointTests.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.alipay.sofa.boot.actuator.startup; + +import com.alipay.sofa.boot.startup.BeanInitInfo; +import com.alipay.sofa.boot.startup.BeanStat; +import com.alipay.sofa.boot.startup.StartupOptimizer; +import com.alipay.sofa.boot.startup.StartupReporter; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.support.GenericApplicationContext; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link StartupOptimizationEndpoint}. + * + * @author OpenAI + */ +public class StartupOptimizationEndpointTests { + + @Test + void analyzeAndSlowBeans() { + StartupReporter startupReporter = new StartupReporter(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("slow", new RootBeanDefinition(Object.class)); + startupReporter.addCommonStartupStat(beanStat("slow", 600)); + StartupOptimizationEndpoint endpoint = new StartupOptimizationEndpoint( + new StartupOptimizer(startupReporter), context); + + assertThat(endpoint.analyze().getRecommendations()).hasSize(1); + List slowBeans = endpoint.slowBeans("slow-beans", 1); + + assertThat(slowBeans).extracting(BeanInitInfo::getBeanName).containsExactly("slow"); + } + + @Test + void rejectsUnsupportedSelector() { + StartupReporter startupReporter = new StartupReporter(); + StartupOptimizationEndpoint endpoint = new StartupOptimizationEndpoint( + new StartupOptimizer(startupReporter), new GenericApplicationContext()); + + assertThatIllegalArgumentException().isThrownBy(() -> endpoint.slowBeans("unknown", 1)); + } + + private BeanStat beanStat(String beanName, long cost) { + BeanStat beanStat = new BeanStat(); + beanStat.setType(StartupReporter.SPRING_BEANS_INSTANTIATE); + beanStat.setName(beanName); + beanStat.setCost(cost); + beanStat.setBeanClassName(Object.class.getName()); + return beanStat; + } +} diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/runtime/AsyncInitProperties.java b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/runtime/AsyncInitProperties.java new file mode 100644 index 000000000..21d2722ef --- /dev/null +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/runtime/AsyncInitProperties.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.alipay.sofa.boot.autoconfigure.runtime; + +import com.alipay.sofa.runtime.async.AsyncInitAutoMode; + +/** + * Configuration properties for async bean initialization. + * + * @author OpenAI + */ +public class AsyncInitProperties { + + /** + * Whether async initialization is enabled. + */ + private boolean enabled = true; + + /** + * Core async init executor size. + */ + private int corePoolSize = Runtime.getRuntime().availableProcessors(); + + /** + * Max async init executor size. + */ + private int maxPoolSize = corePoolSize * 2; + + /** + * Async init executor queue capacity. + */ + private int queueCapacity = 100; + + /** + * Timeout in milliseconds to wait for async init tasks. + */ + private long timeoutMillis = 30000; + + /** + * Automatic candidate detection mode. + */ + private AsyncInitAutoMode autoMode = AsyncInitAutoMode.CONSERVATIVE; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getCorePoolSize() { + return corePoolSize; + } + + public void setCorePoolSize(int corePoolSize) { + this.corePoolSize = corePoolSize; + } + + public int getMaxPoolSize() { + return maxPoolSize; + } + + public void setMaxPoolSize(int maxPoolSize) { + this.maxPoolSize = maxPoolSize; + } + + public int getQueueCapacity() { + return queueCapacity; + } + + public void setQueueCapacity(int queueCapacity) { + this.queueCapacity = queueCapacity; + } + + public long getTimeoutMillis() { + return timeoutMillis; + } + + public void setTimeoutMillis(long timeoutMillis) { + this.timeoutMillis = timeoutMillis; + } + + public AsyncInitAutoMode getAutoMode() { + return autoMode; + } + + public void setAutoMode(AsyncInitAutoMode autoMode) { + this.autoMode = autoMode; + } +} diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/runtime/SofaRuntimeAutoConfiguration.java b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/runtime/SofaRuntimeAutoConfiguration.java index 4227b2ec9..07ed9f331 100644 --- a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/runtime/SofaRuntimeAutoConfiguration.java +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/runtime/SofaRuntimeAutoConfiguration.java @@ -53,6 +53,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; @@ -64,7 +65,9 @@ import org.springframework.util.Assert; import java.util.HashSet; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -160,30 +163,50 @@ public ComponentContextRefreshInterceptor componentContextRefreshInterceptor(Sof return new ComponentContextRefreshInterceptor(sofaRuntimeManager); } + @Bean + @ConditionalOnMissingBean + public static AsyncInitProperties asyncInitProperties(Environment environment) { + AsyncInitProperties asyncInitProperties = new AsyncInitProperties(); + Binder.get(environment).bind("sofa.boot.async-init", + Bindable.ofInstance(asyncInitProperties)); + return asyncInitProperties; + } + @Configuration(proxyBeanMethods = false) @ConditionalOnSwitch(value = "runtimeAsyncInit") + @ConditionalOnProperty(prefix = "sofa.boot.async-init", name = "enabled", havingValue = "true", matchIfMissing = true) static class AsyncInitConfiguration { @Bean @ConditionalOnMissingBean - public static AsyncInitMethodManager asyncInitMethodManager() { - return new AsyncInitMethodManager(); + public static AsyncInitMethodManager asyncInitMethodManager(AsyncInitProperties asyncInitProperties) { + return new AsyncInitMethodManager(asyncInitProperties.getTimeoutMillis()); } @Bean(AsyncInitMethodManager.ASYNC_INIT_METHOD_EXECUTOR_BEAN_NAME) @ConditionalOnMissingBean(name = AsyncInitMethodManager.ASYNC_INIT_METHOD_EXECUTOR_BEAN_NAME) @Conditional(OnVirtualThreadStartupDisableCondition.class) - public Supplier asyncInitMethodExecutor(SofaRuntimeProperties sofaRuntimeProperties) { + public Supplier asyncInitMethodExecutor(SofaRuntimeProperties sofaRuntimeProperties, + AsyncInitProperties asyncInitProperties, + Environment environment) { return ()-> { - int coreSize = sofaRuntimeProperties.getAsyncInitExecutorCoreSize(); - int maxSize = sofaRuntimeProperties.getAsyncInitExecutorMaxSize(); + int coreSize = getCorePoolSize(sofaRuntimeProperties, asyncInitProperties, + environment); + int maxSize = getMaxPoolSize(sofaRuntimeProperties, asyncInitProperties, + environment); + int queueCapacity = asyncInitProperties.getQueueCapacity(); Assert.isTrue(coreSize >= 0, "executorCoreSize must no less than zero"); - Assert.isTrue(maxSize >= 0, "executorMaxSize must no less than zero"); + Assert.isTrue(maxSize > 0, "executorMaxSize must greater than zero"); + Assert.isTrue(maxSize >= coreSize, "executorMaxSize must no less than executorCoreSize"); + Assert.isTrue(queueCapacity >= 0, "queueCapacity must no less than zero"); - LOGGER.info("create async-init-bean thread pool, corePoolSize: {}, maxPoolSize: {}.", - coreSize, maxSize); + LOGGER.info("create async-init-bean thread pool, corePoolSize: {}, maxPoolSize: {}, queueCapacity: {}.", + coreSize, maxSize, queueCapacity); + BlockingQueue workQueue = queueCapacity > 0 + ? new LinkedBlockingQueue<>(queueCapacity) + : new SynchronousQueue<>(); return new SofaThreadPoolExecutor(coreSize, maxSize, 30, TimeUnit.SECONDS, - new SynchronousQueue<>(), new NamedThreadFactory("async-init-bean"), + workQueue, new NamedThreadFactory("async-init-bean"), new ThreadPoolExecutor.CallerRunsPolicy(), "async-init-bean", SofaBootConstants.SOFA_BOOT_SPACE_NAME); }; @@ -207,8 +230,28 @@ public static AsyncProxyBeanPostProcessor asyncProxyBeanPostProcessor(AsyncInitM @Bean @ConditionalOnMissingBean - public static AsyncInitBeanFactoryPostProcessor asyncInitBeanFactoryPostProcessor() { - return new AsyncInitBeanFactoryPostProcessor(); + public static AsyncInitBeanFactoryPostProcessor asyncInitBeanFactoryPostProcessor(AsyncInitProperties asyncInitProperties) { + return new AsyncInitBeanFactoryPostProcessor( + new com.alipay.sofa.runtime.async.SmartAsyncInitAnalyzer(), + asyncInitProperties.getAutoMode()); + } + + private int getCorePoolSize(SofaRuntimeProperties sofaRuntimeProperties, + AsyncInitProperties asyncInitProperties, Environment environment) { + if (!environment.containsProperty("sofa.boot.async-init.core-pool-size") + && environment.containsProperty("sofa.boot.runtime.asyncInitExecutorCoreSize")) { + return sofaRuntimeProperties.getAsyncInitExecutorCoreSize(); + } + return asyncInitProperties.getCorePoolSize(); + } + + private int getMaxPoolSize(SofaRuntimeProperties sofaRuntimeProperties, + AsyncInitProperties asyncInitProperties, Environment environment) { + if (!environment.containsProperty("sofa.boot.async-init.max-pool-size") + && environment.containsProperty("sofa.boot.runtime.asyncInitExecutorMaxSize")) { + return sofaRuntimeProperties.getAsyncInitExecutorMaxSize(); + } + return asyncInitProperties.getMaxPoolSize(); } } } diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/runtime/SofaRuntimeAutoConfigurationTests.java b/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/runtime/SofaRuntimeAutoConfigurationTests.java index c6ab92b71..c9fa768cf 100644 --- a/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/runtime/SofaRuntimeAutoConfigurationTests.java +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/runtime/SofaRuntimeAutoConfigurationTests.java @@ -17,6 +17,7 @@ package com.alipay.sofa.boot.autoconfigure.runtime; import com.alipay.sofa.runtime.async.AsyncInitMethodManager; +import com.alipay.sofa.runtime.async.AsyncInitAutoMode; import com.alipay.sofa.runtime.context.ComponentContextRefreshInterceptor; import com.alipay.sofa.runtime.proxy.ProxyBeanFactoryPostProcessor; import com.alipay.sofa.runtime.spi.binding.BindingAdapterFactory; @@ -130,6 +131,45 @@ public void customAsyncInitMethodManager() { }); } + @Test + public void customAsyncInitProperties() { + this.contextRunner + .withPropertyValues("sofa.boot.async-init.corePoolSize=4") + .withPropertyValues("sofa.boot.async-init.maxPoolSize=8") + .withPropertyValues("sofa.boot.async-init.queueCapacity=7") + .withPropertyValues("sofa.boot.async-init.timeoutMillis=1234") + .withPropertyValues("sofa.boot.async-init.autoMode=AGGRESSIVE") + .run((context) -> { + AsyncInitProperties asyncInitProperties = context + .getBean(AsyncInitProperties.class); + assertThat(asyncInitProperties.getCorePoolSize()).isEqualTo(4); + assertThat(asyncInitProperties.getMaxPoolSize()).isEqualTo(8); + assertThat(asyncInitProperties.getQueueCapacity()).isEqualTo(7); + assertThat(asyncInitProperties.getTimeoutMillis()).isEqualTo(1234); + assertThat(asyncInitProperties.getAutoMode()).isEqualTo(AsyncInitAutoMode.AGGRESSIVE); + assertThat(context.getBean(AsyncInitMethodManager.class).getTimeoutMillis()) + .isEqualTo(1234); + + ExecutorService threadPoolExecutor = (ExecutorService) context.getBean( + Supplier.class, AsyncInitMethodManager.ASYNC_INIT_METHOD_EXECUTOR_BEAN_NAME) + .get(); + assertThat(threadPoolExecutor).isInstanceOf(ThreadPoolExecutor.class); + ThreadPoolExecutor executor = (ThreadPoolExecutor) threadPoolExecutor; + assertThat(executor.getCorePoolSize()).isEqualTo(4); + assertThat(executor.getMaximumPoolSize()).isEqualTo(8); + assertThat(executor.getQueue().remainingCapacity()).isEqualTo(7); + }); + } + + @Test + public void disableAsyncInitProperties() { + this.contextRunner.withPropertyValues("sofa.boot.async-init.enabled=false") + .run((context) -> assertThat(context) + .doesNotHaveBean(AsyncInitMethodManager.class) + .doesNotHaveBean(AsyncProxyBeanPostProcessor.class) + .doesNotHaveBean(AsyncInitBeanFactoryPostProcessor.class)); + } + @Test @EnabledOnJre(JRE.JAVA_21) public void useAsyncInitMethodVirtualExecutor() { diff --git a/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/async/AsyncInitAutoMode.java b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/async/AsyncInitAutoMode.java new file mode 100644 index 000000000..47d625651 --- /dev/null +++ b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/async/AsyncInitAutoMode.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.alipay.sofa.runtime.async; + +/** + * Automatic async init candidate detection mode. + * + * @author OpenAI + */ +public enum AsyncInitAutoMode { + + /** + * Disable automatic candidate detection. + */ + OFF, + + /** + * Only beans with no dependency metadata and no instance fields are detected. + */ + CONSERVATIVE, + + /** + * Analyze bean metadata and injection points to detect safe candidates. + */ + AGGRESSIVE +} diff --git a/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/async/AsyncInitMethodManager.java b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/async/AsyncInitMethodManager.java index b93ed6ec6..0e852eb2a 100644 --- a/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/async/AsyncInitMethodManager.java +++ b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/async/AsyncInitMethodManager.java @@ -32,6 +32,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -49,16 +50,28 @@ public class AsyncInitMethodManager implements PriorityOrdered, public static final String ASYNC_INIT_METHOD_NAME = "async-init-method-name"; + public static final String ASYNC_INIT_DISABLED_ATTRIBUTE = "async-init-disabled"; + private final AtomicReference executorServiceRef = new AtomicReference<>(); private final Map> asyncInitBeanNameMap = new ConcurrentHashMap<>(); private final List> futures = new ArrayList<>(); + private final long timeoutMillis; + private ApplicationContext applicationContext; private boolean startUpFinish = false; + public AsyncInitMethodManager() { + this(0); + } + + public AsyncInitMethodManager(long timeoutMillis) { + this.timeoutMillis = timeoutMillis; + } + @Override public void onApplicationEvent(ContextRefreshedEvent event) { if (applicationContext.equals(event.getApplicationContext())) { @@ -96,7 +109,11 @@ private ExecutorService createAsyncExecutorService() { void ensureAsyncTasksFinish() { for (Future future : futures) { try { - future.get(); + if (timeoutMillis > 0) { + future.get(timeoutMillis, TimeUnit.MILLISECONDS); + } else { + future.get(); + } } catch (Throwable e) { throw new RuntimeException("Async init task finish fail", e); } @@ -128,4 +145,8 @@ public String findAsyncInitMethod(ConfigurableListableBeanFactory beanFactory, S return map.get(beanName); } } + + public long getTimeoutMillis() { + return timeoutMillis; + } } diff --git a/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/async/SmartAsyncInitAnalyzer.java b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/async/SmartAsyncInitAnalyzer.java new file mode 100644 index 000000000..9ae12f54b --- /dev/null +++ b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/async/SmartAsyncInitAnalyzer.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.alipay.sofa.runtime.async; + +import com.alipay.sofa.boot.util.BeanDefinitionUtil; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.ManagedSet; +import org.springframework.util.StringUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static com.alipay.sofa.runtime.async.AsyncInitMethodManager.ASYNC_INIT_DISABLED_ATTRIBUTE; +import static com.alipay.sofa.runtime.async.AsyncInitMethodManager.ASYNC_INIT_METHOD_NAME; + +/** + * Analyzer for bean definitions that can safely move their init method to async startup. + * + * @author OpenAI + */ +public class SmartAsyncInitAnalyzer { + + private static final Set STATEFUL_ANNOTATION_NAMES = Set + .of("javax.ejb.Stateful", + "jakarta.ejb.Stateful", + "org.springframework.boot.context.properties.ConfigurationProperties", + "org.springframework.context.annotation.Configuration"); + + /** + * Analyze bean definitions using conservative mode. + * @param beanFactory bean factory to inspect + * @return async init candidate bean names + */ + public List analyzeAsyncCandidates(ConfigurableListableBeanFactory beanFactory) { + return analyzeAsyncCandidates(beanFactory, AsyncInitAutoMode.CONSERVATIVE); + } + + /** + * Analyze bean definitions using the given auto mode. + * @param beanFactory bean factory to inspect + * @param autoMode auto detection mode + * @return async init candidate bean names + */ + public List analyzeAsyncCandidates(ConfigurableListableBeanFactory beanFactory, + AsyncInitAutoMode autoMode) { + List candidates = new ArrayList<>(); + if (autoMode == null || autoMode == AsyncInitAutoMode.OFF) { + return candidates; + } + for (String beanName : beanFactory.getBeanDefinitionNames()) { + BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); + if (isAsyncCandidate(beanFactory, beanName, beanDefinition, autoMode)) { + candidates.add(beanName); + } + } + return candidates; + } + + /** + * Check whether a bean definition is safe to initialize asynchronously. + * @param beanFactory bean factory to inspect + * @param beanName bean name + * @param beanDefinition bean definition + * @param autoMode auto detection mode + * @return true if the bean can be automatically marked async + */ + public boolean isAsyncCandidate(ConfigurableListableBeanFactory beanFactory, String beanName, + BeanDefinition beanDefinition, AsyncInitAutoMode autoMode) { + if (autoMode == null || autoMode == AsyncInitAutoMode.OFF) { + return false; + } + if (!StringUtils.hasText(beanDefinition.getInitMethodName())) { + return false; + } + if (StringUtils.hasText((String) beanDefinition.getAttribute(ASYNC_INIT_METHOD_NAME))) { + return false; + } + if (Boolean.TRUE.equals(beanDefinition.getAttribute(ASYNC_INIT_DISABLED_ATTRIBUTE))) { + return false; + } + if (beanDefinition.isAbstract() || beanDefinition.isLazyInit() + || !beanDefinition.isSingleton()) { + return false; + } + return isStatelessService(beanDefinition, autoMode) + && !hasMandatoryDependencies(beanFactory, beanName, beanDefinition); + } + + private boolean isStatelessService(BeanDefinition beanDefinition, AsyncInitAutoMode autoMode) { + Class beanClass = BeanDefinitionUtil.resolveBeanClassType(beanDefinition); + if (beanClass == null || beanClass.isInterface() || beanClass.isAnnotation()) { + return false; + } + if (org.springframework.beans.factory.FactoryBean.class.isAssignableFrom(beanClass)) { + return false; + } + if (hasStatefulAnnotations(beanClass)) { + return false; + } + if (autoMode == AsyncInitAutoMode.CONSERVATIVE) { + return !hasInstanceFields(beanClass); + } + return !hasMutableStateFields(beanClass); + } + + private boolean hasStatefulAnnotations(Class beanClass) { + for (Annotation annotation : beanClass.getAnnotations()) { + if (STATEFUL_ANNOTATION_NAMES.contains(annotation.annotationType().getName())) { + return true; + } + } + return false; + } + + private boolean hasMandatoryDependencies(ConfigurableListableBeanFactory beanFactory, + String beanName, BeanDefinition beanDefinition) { + if (beanDefinition.getDependsOn() != null && beanDefinition.getDependsOn().length > 0) { + return true; + } + if (StringUtils.hasText(beanDefinition.getFactoryBeanName())) { + return true; + } + ConstructorArgumentValues constructorArgumentValues = beanDefinition + .getConstructorArgumentValues(); + if (!constructorArgumentValues.isEmpty()) { + return true; + } + for (PropertyValue propertyValue : beanDefinition.getPropertyValues() + .getPropertyValueList()) { + if (isBeanReference(propertyValue.getValue())) { + return true; + } + } + if (beanFactory.getDependenciesForBean(beanName).length > 0) { + return true; + } + Class beanClass = BeanDefinitionUtil.resolveBeanClassType(beanDefinition); + return beanClass != null && hasMandatoryInjectionPoints(beanClass); + } + + private boolean isBeanReference(Object value) { + if (value instanceof RuntimeBeanReference || value instanceof BeanDefinitionHolder + || value instanceof BeanDefinition) { + return true; + } + if (value instanceof ManagedList list) { + return list.stream().anyMatch(this::isBeanReference); + } + if (value instanceof ManagedSet set) { + return set.stream().anyMatch(this::isBeanReference); + } + if (value instanceof ManagedMap map) { + return map.entrySet().stream() + .anyMatch(entry -> isBeanReference(entry.getKey()) || isBeanReference(entry.getValue())); + } + return false; + } + + private boolean hasMandatoryInjectionPoints(Class beanClass) { + for (Constructor constructor : beanClass.getDeclaredConstructors()) { + if (constructor.getParameterCount() > 0 && isMandatoryInjectionPoint(constructor)) { + return true; + } + } + for (Field field : beanClass.getDeclaredFields()) { + if (isMandatoryInjectionPoint(field)) { + return true; + } + } + for (Method method : beanClass.getDeclaredMethods()) { + if (method.getParameterCount() > 0 && isMandatoryInjectionPoint(method)) { + return true; + } + } + return hasSingleMandatoryConstructor(beanClass); + } + + private boolean hasSingleMandatoryConstructor(Class beanClass) { + Constructor[] constructors = beanClass.getDeclaredConstructors(); + if (constructors.length != 1) { + return false; + } + Constructor constructor = constructors[0]; + return constructor.getParameterCount() > 0 && !hasNoArgConstructor(beanClass); + } + + private boolean hasNoArgConstructor(Class beanClass) { + for (Constructor constructor : beanClass.getDeclaredConstructors()) { + if (constructor.getParameterCount() == 0) { + return true; + } + } + return false; + } + + private boolean isMandatoryInjectionPoint(Member member) { + for (Annotation annotation : getAnnotations(member)) { + Class annotationType = annotation.annotationType(); + String annotationName = annotationType.getName(); + if (Autowired.class.getName().equals(annotationName)) { + return isAutowiredRequired(annotation); + } + if ("javax.annotation.Resource".equals(annotationName) + || "jakarta.annotation.Resource".equals(annotationName) + || "javax.inject.Inject".equals(annotationName) + || "jakarta.inject.Inject".equals(annotationName)) { + return true; + } + } + return false; + } + + private Annotation[] getAnnotations(Member member) { + if (member instanceof Field field) { + return field.getAnnotations(); + } + if (member instanceof Method method) { + return method.getAnnotations(); + } + return ((Constructor) member).getAnnotations(); + } + + private boolean isAutowiredRequired(Annotation annotation) { + try { + Method requiredMethod = annotation.annotationType().getMethod("required"); + return Boolean.TRUE.equals(requiredMethod.invoke(annotation)); + } catch (Throwable throwable) { + return true; + } + } + + private boolean hasInstanceFields(Class beanClass) { + for (Field field : beanClass.getDeclaredFields()) { + if (!field.isSynthetic() && !Modifier.isStatic(field.getModifiers())) { + return true; + } + } + return false; + } + + private boolean hasMutableStateFields(Class beanClass) { + for (Field field : beanClass.getDeclaredFields()) { + if (field.isSynthetic() || Modifier.isStatic(field.getModifiers()) + || Modifier.isFinal(field.getModifiers())) { + continue; + } + if (!isMandatoryInjectionPoint(field)) { + return true; + } + } + return false; + } +} diff --git a/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/spring/AsyncInitBeanFactoryPostProcessor.java b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/spring/AsyncInitBeanFactoryPostProcessor.java index b50c4bde7..4222aec79 100644 --- a/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/spring/AsyncInitBeanFactoryPostProcessor.java +++ b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/spring/AsyncInitBeanFactoryPostProcessor.java @@ -23,6 +23,8 @@ import com.alipay.sofa.boot.log.SofaBootLoggerFactory; import com.alipay.sofa.boot.util.BeanDefinitionUtil; import com.alipay.sofa.runtime.api.annotation.SofaAsyncInit; +import com.alipay.sofa.runtime.async.AsyncInitAutoMode; +import com.alipay.sofa.runtime.async.SmartAsyncInitAnalyzer; import org.slf4j.Logger; import org.springframework.beans.BeansException; import org.springframework.beans.FatalBeanException; @@ -52,6 +54,7 @@ import java.util.function.Function; import java.util.stream.Collectors; +import static com.alipay.sofa.runtime.async.AsyncInitMethodManager.ASYNC_INIT_DISABLED_ATTRIBUTE; import static com.alipay.sofa.runtime.async.AsyncInitMethodManager.ASYNC_INIT_METHOD_NAME; /** @@ -67,13 +70,31 @@ public class AsyncInitBeanFactoryPostProcessor implements BeanFactoryPostProcess private static final Logger LOGGER = SofaBootLoggerFactory .getLogger(AsyncInitBeanFactoryPostProcessor.class); + private final SmartAsyncInitAnalyzer smartAsyncInitAnalyzer; + + private final AsyncInitAutoMode autoMode; + private AnnotationWrapper annotationWrapper; + public AsyncInitBeanFactoryPostProcessor() { + this(new SmartAsyncInitAnalyzer(), AsyncInitAutoMode.OFF); + } + + public AsyncInitBeanFactoryPostProcessor(SmartAsyncInitAnalyzer smartAsyncInitAnalyzer, + AsyncInitAutoMode autoMode) { + this.smartAsyncInitAnalyzer = smartAsyncInitAnalyzer; + this.autoMode = autoMode; + } + @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { Arrays.stream(beanFactory.getBeanDefinitionNames()) .collect(Collectors.toMap(Function.identity(), beanFactory::getBeanDefinition)) .forEach(this::scanAsyncInitBeanDefinition); + smartAsyncInitAnalyzer.analyzeAsyncCandidates(beanFactory, autoMode).forEach(beanName -> { + BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); + beanDefinition.setAttribute(ASYNC_INIT_METHOD_NAME, beanDefinition.getInitMethodName()); + }); } /** @@ -178,7 +199,9 @@ private void registerAsyncInitBean(SofaAsyncInit sofaAsyncInitAnnotation, sofaAsyncInitAnnotation = annotationWrapper.wrap(sofaAsyncInitAnnotation); String initMethodName = beanDefinition.getInitMethodName(); - if (sofaAsyncInitAnnotation.value() && StringUtils.hasText(initMethodName)) { + if (!sofaAsyncInitAnnotation.value()) { + beanDefinition.setAttribute(ASYNC_INIT_DISABLED_ATTRIBUTE, true); + } else if (StringUtils.hasText(initMethodName)) { beanDefinition.setAttribute(ASYNC_INIT_METHOD_NAME, initMethodName); } } diff --git a/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/spring/parser/AsyncInitBeanDefinitionDecorator.java b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/spring/parser/AsyncInitBeanDefinitionDecorator.java index aa5f471d1..b8d348bd2 100644 --- a/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/spring/parser/AsyncInitBeanDefinitionDecorator.java +++ b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/main/java/com/alipay/sofa/runtime/spring/parser/AsyncInitBeanDefinitionDecorator.java @@ -24,6 +24,7 @@ import org.w3c.dom.Attr; import org.w3c.dom.Node; +import static com.alipay.sofa.runtime.async.AsyncInitMethodManager.ASYNC_INIT_DISABLED_ATTRIBUTE; import static com.alipay.sofa.runtime.async.AsyncInitMethodManager.ASYNC_INIT_METHOD_NAME; /** @@ -39,6 +40,7 @@ public class AsyncInitBeanDefinitionDecorator implements BeanDefinitionDecorator public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) { if (!Boolean.TRUE.toString().equalsIgnoreCase(((Attr) node).getValue())) { + definition.getBeanDefinition().setAttribute(ASYNC_INIT_DISABLED_ATTRIBUTE, true); return definition; } diff --git a/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/test/java/com/alipay/sofa/runtime/async/AsyncInitMethodManagerTests.java b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/test/java/com/alipay/sofa/runtime/async/AsyncInitMethodManagerTests.java index 4c77e8302..0279f9b9d 100644 --- a/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/test/java/com/alipay/sofa/runtime/async/AsyncInitMethodManagerTests.java +++ b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/test/java/com/alipay/sofa/runtime/async/AsyncInitMethodManagerTests.java @@ -102,4 +102,10 @@ void registerAsyncInitBean() { asyncInitMethodName); } + @Test + void timeoutMillis() { + AsyncInitMethodManager manager = new AsyncInitMethodManager(10); + assertThat(manager.getTimeoutMillis()).isEqualTo(10); + } + } diff --git a/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/test/java/com/alipay/sofa/runtime/async/SmartAsyncInitAnalyzerTests.java b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/test/java/com/alipay/sofa/runtime/async/SmartAsyncInitAnalyzerTests.java new file mode 100644 index 000000000..cabe33ef8 --- /dev/null +++ b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/test/java/com/alipay/sofa/runtime/async/SmartAsyncInitAnalyzerTests.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.alipay.sofa.runtime.async; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +import static com.alipay.sofa.runtime.async.AsyncInitMethodManager.ASYNC_INIT_DISABLED_ATTRIBUTE; +import static com.alipay.sofa.runtime.async.AsyncInitMethodManager.ASYNC_INIT_METHOD_NAME; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SmartAsyncInitAnalyzer}. + * + * @author OpenAI + */ +public class SmartAsyncInitAnalyzerTests { + + private final SmartAsyncInitAnalyzer analyzer = new SmartAsyncInitAnalyzer(); + + @Test + void conservativeModeOnlyAcceptsStatelessBeanWithoutDependencies() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + register(beanFactory, "candidate", StatelessBean.class, "init"); + register(beanFactory, "noInit", StatelessBean.class, null); + register(beanFactory, "stateful", StatefulBean.class, "init"); + register(beanFactory, "constructorDependency", ConstructorDependencyBean.class, "init"); + register(beanFactory, "configurationProperties", PropertiesBean.class, "init"); + + List candidates = analyzer.analyzeAsyncCandidates(beanFactory, + AsyncInitAutoMode.CONSERVATIVE); + + assertThat(candidates).containsExactly("candidate"); + } + + @Test + void aggressiveModeRejectsPropertyAndRegisteredDependencies() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + register(beanFactory, "candidate", ImmutableBean.class, "init"); + RootBeanDefinition propertyDependency = register(beanFactory, "propertyDependency", + StatelessBean.class, "init"); + propertyDependency.getPropertyValues().add("dependency", + new RuntimeBeanReference("dependency")); + register(beanFactory, "registeredDependency", StatelessBean.class, "init"); + beanFactory.registerDependentBean("dependency", "registeredDependency"); + + List candidates = analyzer.analyzeAsyncCandidates(beanFactory, + AsyncInitAutoMode.AGGRESSIVE); + + assertThat(candidates).containsExactly("candidate"); + } + + @Test + void skipsExplicitAsyncAndExplicitDisabledBeans() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition explicitAsync = register(beanFactory, "explicitAsync", + StatelessBean.class, "init"); + explicitAsync.setAttribute(ASYNC_INIT_METHOD_NAME, "init"); + RootBeanDefinition disabled = register(beanFactory, "disabled", StatelessBean.class, "init"); + disabled.setAttribute(ASYNC_INIT_DISABLED_ATTRIBUTE, true); + + List candidates = analyzer.analyzeAsyncCandidates(beanFactory, + AsyncInitAutoMode.CONSERVATIVE); + + assertThat(candidates).isEmpty(); + } + + @Test + void offModeReturnsNoCandidates() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + register(beanFactory, "candidate", StatelessBean.class, "init"); + + assertThat(analyzer.analyzeAsyncCandidates(beanFactory, AsyncInitAutoMode.OFF)).isEmpty(); + } + + private RootBeanDefinition register(DefaultListableBeanFactory beanFactory, String beanName, + Class beanClass, String initMethodName) { + RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass); + beanDefinition.setInitMethodName(initMethodName); + beanFactory.registerBeanDefinition(beanName, beanDefinition); + return beanDefinition; + } + + static class StatelessBean { + + public void init() { + } + } + + static class StatefulBean { + + private String value; + + public void init() { + } + } + + static class ConstructorDependencyBean { + + ConstructorDependencyBean(StatelessBean dependency) { + } + + public void init() { + } + } + + static class ImmutableBean { + + private final int value = 1; + + public void init() { + } + } + + @ConfigurationProperties("test") + static class PropertiesBean { + + public void init() { + } + } +} diff --git a/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/test/java/com/alipay/sofa/runtime/spring/AsyncInitBeanFactoryPostProcessorTests.java b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/test/java/com/alipay/sofa/runtime/spring/AsyncInitBeanFactoryPostProcessorTests.java index 856d0e1a8..19ee97f63 100644 --- a/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/test/java/com/alipay/sofa/runtime/spring/AsyncInitBeanFactoryPostProcessorTests.java +++ b/sofa-boot-project/sofa-boot-core/runtime-sofa-boot/src/test/java/com/alipay/sofa/runtime/spring/AsyncInitBeanFactoryPostProcessorTests.java @@ -17,6 +17,8 @@ package com.alipay.sofa.runtime.spring; import com.alipay.sofa.runtime.api.annotation.SofaAsyncInit; +import com.alipay.sofa.runtime.async.AsyncInitAutoMode; +import com.alipay.sofa.runtime.async.SmartAsyncInitAnalyzer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.BeanDefinition; @@ -80,6 +82,22 @@ public void parseAnnotationOnClassWithFalse() { assertThat(rootBeanDefinition.getAttribute(ASYNC_INIT_METHOD_NAME)).isNull(); } + @Test + public void parseSmartAsyncCandidate() { + GenericApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean(AsyncInitBeanFactoryPostProcessor.class, + () -> new AsyncInitBeanFactoryPostProcessor(new SmartAsyncInitAnalyzer(), + AsyncInitAutoMode.CONSERVATIVE)); + RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(); + rootBeanDefinition.setBeanClass(SmartCandidateClass.class); + rootBeanDefinition.setInitMethodName("init"); + context.registerBeanDefinition("bean", rootBeanDefinition); + context.refresh(); + + assertThat(rootBeanDefinition.getAttribute(ASYNC_INIT_METHOD_NAME)).isEqualTo("init"); + context.close(); + } + @SofaAsyncInit static class NormalClass { @@ -105,4 +123,11 @@ public NormalClass normalClass() { return new NormalClass(); } } + + static class SmartCandidateClass { + + public void init() { + + } + } } diff --git a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/startup/BeanInitInfo.java b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/startup/BeanInitInfo.java new file mode 100644 index 000000000..dfeaeea8e --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/startup/BeanInitInfo.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.alipay.sofa.boot.startup; + +import java.util.HashMap; +import java.util.Map; + +/** + * Bean initialization information for startup optimization. + * + * @author OpenAI + */ +public class BeanInitInfo { + + private String beanName; + + private String beanClassName; + + private long initTime; + + private boolean async; + + private Map attributes = new HashMap<>(); + + public String getBeanName() { + return beanName; + } + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + public String getBeanClassName() { + return beanClassName; + } + + public void setBeanClassName(String beanClassName) { + this.beanClassName = beanClassName; + } + + public long getInitTime() { + return initTime; + } + + public void setInitTime(long initTime) { + this.initTime = initTime; + } + + public boolean isAsync() { + return async; + } + + public void setAsync(boolean async) { + this.async = async; + } + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } +} diff --git a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/startup/StartupOptimizer.java b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/startup/StartupOptimizer.java new file mode 100644 index 000000000..71ddcaee0 --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/startup/StartupOptimizer.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.alipay.sofa.boot.startup; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Analyzer that creates startup bottleneck reports and async init recommendations. + * + * @author OpenAI + */ +public class StartupOptimizer { + + static final String ASYNC_INIT_METHOD_NAME = "async-init-method-name"; + + private static final long ASYNC_RECOMMENDATION_THRESHOLD = 500; + + private final StartupReporter startupReporter; + + public StartupOptimizer(StartupReporter startupReporter) { + this.startupReporter = startupReporter; + } + + public StartupReport analyzeStartupBottlenecks(ApplicationContext context) { + StartupReport report = new StartupReport(); + report.setSequentialBeans(findSequentialBeans(context)); + report.setSlowestBeans(findSlowBeans(context, 10)); + report.setRecommendations(generateRecommendations(report)); + return report; + } + + public List findSequentialBeans(ApplicationContext context) { + return findBeanInitInfos(context).stream().filter(beanInitInfo -> !beanInitInfo.isAsync()) + .collect(Collectors.toList()); + } + + public List findSlowBeans(ApplicationContext context, int top) { + if (top <= 0) { + return List.of(); + } + return findBeanInitInfos(context).stream() + .sorted(Comparator.comparingLong(BeanInitInfo::getInitTime).reversed()).limit(top) + .collect(Collectors.toList()); + } + + public List generateRecommendations(StartupReport report) { + List recommendations = new ArrayList<>(); + for (BeanInitInfo slowBean : report.getSlowestBeans()) { + if (slowBean.getInitTime() > ASYNC_RECOMMENDATION_THRESHOLD && !slowBean.isAsync()) { + recommendations.add(new StartupRecommendation("ASYNC_CANDIDATE", slowBean + .getBeanName(), String.format( + "Bean '%s' init cost %dms, consider adding @SofaAsyncInit.", + slowBean.getBeanName(), slowBean.getInitTime()))); + } + } + return recommendations; + } + + private List findBeanInitInfos(ApplicationContext context) { + List beanInitInfos = new ArrayList<>(); + for (BaseStat stageStat : startupReporter.getStartupStaticsModel().getStageStats()) { + collectBeanInitInfos(stageStat, context, beanInitInfos); + } + return beanInitInfos; + } + + private void collectBeanInitInfos(BaseStat stat, ApplicationContext context, + List beanInitInfos) { + if (stat instanceof BeanStat beanStat + && StartupReporter.SPRING_BEAN_INSTANTIATE_TYPES.contains(beanStat.getType())) { + beanInitInfos.add(toBeanInitInfo(beanStat, context)); + } + if (stat instanceof ChildrenStat childrenStat) { + childrenStat.getChildren() + .forEach(child -> collectBeanInitInfos(child, context, beanInitInfos)); + } + } + + private BeanInitInfo toBeanInitInfo(BeanStat beanStat, ApplicationContext context) { + BeanInitInfo beanInitInfo = new BeanInitInfo(); + beanInitInfo.setBeanName(beanStat.getName()); + beanInitInfo.setBeanClassName(getBeanClassName(beanStat)); + beanInitInfo.setInitTime(beanStat.getCost()); + beanInitInfo.setAttributes(beanStat.getAttributes()); + beanInitInfo.setAsync(isAsyncBean(context, beanStat.getName())); + return beanInitInfo; + } + + private String getBeanClassName(BeanStat beanStat) { + String classType = beanStat.getAttribute("classType"); + if (StringUtils.hasText(classType)) { + return classType; + } + return beanStat.getBeanClassName(); + } + + private boolean isAsyncBean(ApplicationContext context, String beanName) { + if (!(context instanceof ConfigurableApplicationContext configurableApplicationContext) + || !StringUtils.hasText(beanName)) { + return false; + } + ConfigurableListableBeanFactory beanFactory = configurableApplicationContext + .getBeanFactory(); + if (!beanFactory.containsBeanDefinition(beanName)) { + return false; + } + Object asyncInitMethodName = beanFactory.getBeanDefinition(beanName) + .getAttribute(ASYNC_INIT_METHOD_NAME); + return asyncInitMethodName instanceof String methodName && StringUtils.hasText(methodName); + } +} diff --git a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/startup/StartupRecommendation.java b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/startup/StartupRecommendation.java new file mode 100644 index 000000000..e54964edf --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/startup/StartupRecommendation.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.alipay.sofa.boot.startup; + +/** + * Startup optimization recommendation. + * + * @author OpenAI + */ +public class StartupRecommendation { + + private String type; + + private String beanName; + + private String description; + + public StartupRecommendation() { + } + + public StartupRecommendation(String type, String beanName, String description) { + this.type = type; + this.beanName = beanName; + this.description = description; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getBeanName() { + return beanName; + } + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/startup/StartupReport.java b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/startup/StartupReport.java new file mode 100644 index 000000000..539c2f922 --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/startup/StartupReport.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.alipay.sofa.boot.startup; + +import java.util.ArrayList; +import java.util.List; + +/** + * Startup optimization analysis report. + * + * @author OpenAI + */ +public class StartupReport { + + private List sequentialBeans = new ArrayList<>(); + + private List slowestBeans = new ArrayList<>(); + + private List recommendations = new ArrayList<>(); + + public List getSequentialBeans() { + return sequentialBeans; + } + + public void setSequentialBeans(List sequentialBeans) { + this.sequentialBeans = sequentialBeans; + } + + public List getSlowestBeans() { + return slowestBeans; + } + + public void setSlowestBeans(List slowestBeans) { + this.slowestBeans = slowestBeans; + } + + public List getRecommendations() { + return recommendations; + } + + public void setRecommendations(List recommendations) { + this.recommendations = recommendations; + } +} diff --git a/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/startup/StartupOptimizerTests.java b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/startup/StartupOptimizerTests.java new file mode 100644 index 000000000..39559e6f9 --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/startup/StartupOptimizerTests.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.alipay.sofa.boot.startup; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.support.GenericApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StartupOptimizer}. + * + * @author OpenAI + */ +public class StartupOptimizerTests { + + @Test + void analyzeStartupBottlenecksGeneratesSlowBeanRecommendation() { + StartupReporter startupReporter = new StartupReporter(); + StartupOptimizer startupOptimizer = new StartupOptimizer(startupReporter); + GenericApplicationContext context = new GenericApplicationContext(); + registerBeanDefinition(context, "slowBean", false); + registerBeanDefinition(context, "asyncBean", true); + startupReporter.addCommonStartupStat(beanStat("slowBean", 600)); + startupReporter.addCommonStartupStat(beanStat("asyncBean", 700)); + + StartupReport report = startupOptimizer.analyzeStartupBottlenecks(context); + + assertThat(report.getSlowestBeans()).extracting(BeanInitInfo::getBeanName) + .containsExactly("asyncBean", "slowBean"); + assertThat(report.getSequentialBeans()).extracting(BeanInitInfo::getBeanName) + .containsExactly("slowBean"); + assertThat(report.getRecommendations()).hasSize(1); + assertThat(report.getRecommendations().get(0).getBeanName()).isEqualTo("slowBean"); + } + + @Test + void findSlowBeansRespectsTopLimitAndEmptyTop() { + StartupReporter startupReporter = new StartupReporter(); + StartupOptimizer startupOptimizer = new StartupOptimizer(startupReporter); + GenericApplicationContext context = new GenericApplicationContext(); + registerBeanDefinition(context, "one", false); + registerBeanDefinition(context, "two", false); + startupReporter.addCommonStartupStat(beanStat("one", 100)); + startupReporter.addCommonStartupStat(beanStat("two", 200)); + + assertThat(startupOptimizer.findSlowBeans(context, 1)).extracting(BeanInitInfo::getBeanName) + .containsExactly("two"); + assertThat(startupOptimizer.findSlowBeans(context, 0)).isEmpty(); + } + + @Test + void collectsNestedBeanStats() { + StartupReporter startupReporter = new StartupReporter(); + StartupOptimizer startupOptimizer = new StartupOptimizer(startupReporter); + GenericApplicationContext context = new GenericApplicationContext(); + registerBeanDefinition(context, "nested", false); + ChildrenStat root = new ChildrenStat<>(); + root.addChild(beanStat("nested", 300)); + startupReporter.addCommonStartupStat(root); + + assertThat(startupOptimizer.findSequentialBeans(context)).extracting(BeanInitInfo::getBeanName) + .containsExactly("nested"); + } + + private void registerBeanDefinition(GenericApplicationContext context, String beanName, + boolean async) { + RootBeanDefinition beanDefinition = new RootBeanDefinition(Object.class); + if (async) { + beanDefinition.setAttribute(StartupOptimizer.ASYNC_INIT_METHOD_NAME, "init"); + } + context.registerBeanDefinition(beanName, beanDefinition); + } + + private BeanStat beanStat(String beanName, long cost) { + BeanStat beanStat = new BeanStat(); + beanStat.setType(StartupReporter.SPRING_BEANS_INSTANTIATE); + beanStat.setName(beanName); + beanStat.setCost(cost); + beanStat.setBeanClassName(Object.class.getName()); + beanStat.putAttribute("classType", Object.class.getName()); + return beanStat; + } +}