-
Notifications
You must be signed in to change notification settings - Fork 41.1k
Make it possible to opt in to TaskExecutor auto-configuration when an Executor has already been defined #44659
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I've attached a sample project which reproduced the issue. If you run it, you'll see it fails to start due to a missing AsyncTaskExecutor bean. If you comment out the |
It should only back off for My proposal: https://github.com/spring-projects/spring-boot/compare/main...quaff:patch-137?expand=1 |
TaskExecutor will back off for custom Executor before this commit, now it only back off for custom TaskExecutor. Fix spring-projectsGH-44659 Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
TaskExecutor will back off for custom Executor before this commit, now it only back off for custom TaskExecutor. Fix spring-projectsGH-44659 Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
Or It make sense since |
I disagree with that and this bit of the auto-configuration is a bit tricky, I reckon. This is due to the bridge we had before the executor abstraction was available in the JDK. The condition on |
@quaff ScheduledExecutorService was just an example. Any bean of type Executor will cause an app to fail startup if they need the AsyncTaskExecutor.
@snicoll We've found the opposite. Users define a bean for other purposes but expect to still use the AsyncTaskExecutor for other tasks. |
In this case, you can use @Bean(defaultCandidate = false)
@Qualifier("myexecutor")
public ScheduledExecutorService executorService() {
return Executors.newSingleThreadScheduledExecutor();
} This way, you will have both the auto-configured |
I am sure that does happen. Hopefully what follows explain in more details what I meant. @jhoeller and I discussed this one at length as I was trying to get back the context of why we did this that way. The conditions currently aren't ideal but they aren't out of place either.
We could improve the condition to only backoff if no beans named The auto-configuration creates builder that applies the logic of the auto-configuration so you could do something as follows: @Bean
Executor myCustomExecutor() { ... }
@Bean
SimpleAsyncTaskExecutor taskExecutor(SimpleAsyncTaskExecutorBuilder builder) {
return builder.build();
}
This is flagged for team meeting so we'll discuss this ASAP, and hopefully we can find another way that would improve the current situation. |
@snicoll and I discussed this today. As things stand, the best way to retain the auto-configured @Bean(defaultCandidate = false)
@Qualifier("myexecutor")
ScheduledExecutorService executorService() {
return Executors.newSingleThreadScheduledExecutor();
} This requires Spring Framework 6.2 so is only possible in Boot 3.4 and later. For Boot 3.3, you can use the builder to define your own @Bean(name = {"taskExector", "applicationTaskExecutor"})
SimpleAsyncTaskExecutor taskExecutor(SimpleAsyncTaskExecutorBuilder builder) {
return builder.build();
} We'd like to improve this. Unfortunately, it's complicated by existing Framework behavior. Framework will look for a unique bean of a particular type. When there a multiple beans of that type, it will look for a bean of a particular type with a specific name ( A further complication here is that there is overlap between the types that can be used for task scheduling and async task execution. This overlap further hinders any attempt to infer intent from the beans that have bean defined. To avoid problems described above, particularly when upgrading, we think any new behavior will have to be gated by a property. We're currently envisaging two boolean properties, one for task execution and one for scheduling. By setting one or both properties to Focusing on the task execution case as that's the subject of this issue, setting the property to To allow this issue to keep its focus on task execution, I have opened #44806 for the task scheduling side of things. |
I'm not sure whether it was done intentionally or not, but these changes introduce breaking changes: Test to Reproduce:class Gh44659ApplicationTests {
@Test
void taskExecutorBeanNameExists() {
SpringApplication application = new SpringApplication(MyApplication.class);
ConfigurableApplicationContext context = application.run();
// 3.5.0-SNAPSHOT -> FAILED; 3.4.4 -> PASSED
assertThatNoException().isThrownBy(() -> context.getBean("taskExecutor"));
}
@Test
void taskExecutorBeanNameIsReserved() {
// 3.5.0 -> FAILED; 3.4.4 -> PASSED
assertThatException().isThrownBy(
() -> new SpringApplication(MyApplication.class, CustomTaskExecutorConfiguration.class)
.run())
.withStackTraceContaining("Cannot register alias 'taskExecutor' for name 'applicationTaskExecutor'");
}
@Test
void applicationTaskExecutorBeanNameIsReserved() {
// 3.5.0-SNAPSHOT and 3.4.4 both PASSED
assertThatException().isThrownBy(
() -> new SpringApplication(MyApplication.class, ApplicationTaskExecutorConfiguration.class)
.run())
.withStackTraceContaining(
"Invalid bean definition with name 'applicationTaskExecutor' defined in class");
}
@SpringBootApplication
static class MyApplication {
}
@Configuration(proxyBeanMethods = false)
static class CustomTaskExecutorConfiguration {
@Bean(defaultCandidate = false)
Executor taskExecutor() {
return Runnable::run;
}
}
@Configuration(proxyBeanMethods = false)
static class ApplicationTaskExecutorConfiguration {
@Bean(defaultCandidate = false)
Executor applicationTaskExecutor() {
return Runnable::run;
}
}
}
This branch (main...nosan:spring-boot:44659) tries to address both issues while simplifying things a little bit. |
Thanks @nosan.
Yes, that is on purpose as we no longer require to use this name. We automatically configure async processing with the auto-configured one.
I am not sure what you mean by "reserved". How was it reserved before? I am looking at the example and I am not sure I understand what use case the test is trying to simulate. |
This means that if anyone in their codebase is using
From my perspective, this qualifies as a breaking change. Prior to version 3.5.x, the bean was registered, but starting from version 3.5.x, it is no longer registered.
Before these changes, it was not possible to use either /** 3.4.4, not anymore in 3.5.0-SNAPSHOT
* Cannot register alias 'taskExecutor' for name 'applicationTaskExecutor': Alias would override bean definition 'taskExecutor'
* java.lang.IllegalStateException: Cannot register alias 'taskExecutor' for name 'applicationTaskExecutor': Alias would override bean definition *
* 'taskExecutor'
*/
@Test
void taskExecutorBeanNameIsReserved() {
new SpringApplication(MyApplication.class, TaskExecutorConfiguration.class)
.run();
}
@Configuration(proxyBeanMethods = false)
static class TaskExecutorConfiguration {
@Bean(autowireCandidate = false)
Executor taskExecutor() {
return Runnable::run;
}
} Although this may not seem like a significant issue, it creates potential for unpredictable configurations: @Test
void taskExecutor() throws Exception {
SpringApplication application = new SpringApplication(MyApplication.class);
application.setDefaultProperties(Map.of("spring.task.execution.mode", "force",
"spring.task.execution.thread-name-prefix", "auto-task-"));
Future<String> echo = application.run().getBean(TestBean.class).echo("Hello world");
assertThat(echo.get()).contains("auto-task-1");
}
@EnableAsync
@SpringBootApplication
@Import(TestBean.class)
static class MyApplication {
// @Bean(autowireCandidate = false)
// Executor taskExecutor() {
// return new SimpleAsyncTaskExecutor("autowired-candidate-executor");
// }
@Bean
Executor customTaskExecutor() {
return new SimpleAsyncTaskExecutor("customTaskExecutor-");
}
}
static class TestBean {
@Async
Future<String> echo(String text) {
return CompletableFuture.completedFuture(Thread.currentThread().getName() + " " + text);
}
} If you uncomment the In my opinion, if developers want to use a custom task executor while keeping the auto-configured one, they should avoid using the bean names I suggest reverting This also simplifies logic in @ConditionalOnMissingBean(name = AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME,
value = AsyncConfigurer.class)
static class AsyncConfigurerConfiguration {} |
By registering an @Test
void taskExecutor() throws Exception {
SpringApplication application = new SpringApplication(MyApplication.class);
application.setDefaultProperties(Map.of("spring.task.execution.mode", "force",
"spring.task.execution.thread-name-prefix", "auto-task-"));
Future<String> echo = application.run().getBean(TestBean.class).echo("Hello world");
assertThat(echo.get()).contains("customTaskExecutor-");
// Expecting actual:
// "auto-task-1 Hello world"
// to contain:
// "customTaskExecutor-"
// java.lang.AssertionError:
// Expecting actual:
// "auto-task-1 Hello world"
// to contain:
// "customTaskExecutor-"
}
@EnableAsync
@SpringBootApplication
@Import(TestBean.class)
static class MyApplication {
@Bean
@Primary
Executor customTaskExecutor() {
return new SimpleAsyncTaskExecutor("customTaskExecutor-");
}
}
static class TestBean {
@Async
Future<String> echo(String text) {
return CompletableFuture.completedFuture(Thread.currentThread().getName() + " " + text);
}
} |
I think you might take this a little bit backwards.
I can see that, but I am not aware we advertized that the auto-configuration would alias the (auto-configured)
I am not sure I agree. We're talking about bean name. If you were relying on a bean name,
We no longer rely on As you've seen this part of the codebase is quite complex and we're trying to help users by providing them an escape hatch. In summary, I can see that your concerns are as follows:
Hopefully I should have provided an explanation for the first point. As for the second, this is a new feature, and therefore a new behavior so it doesn't qualify as breaking change to me. Based on your feedback, I think we must relax the condition for |
I am not sure I can agree that stopping the registration of the bean's alias Although not all results are relevant, it highlights that developers may be relying on this behavior. This doesn’t account for private repositories or other platforms like Bitbucket or GitLab.
I can prepare a PR to restore the 3.4.x behavior and include additional tests to address the concerns I’ve described above. Let me know if that works for you! |
We don't really consider bean names to be part of the public API. Generally speaking, and regardless of this change, we discourage retrieving an auto-configured bean by name. As @snicoll has said above, Boot's use of the We'll document the change of name in the release notes in case it trips anyone up. That also gives us an opportunity to discourage name-based retrieval. Anyone who has code which cannot be changed and that relies on the |
Unfortunately, with these changes, the @Test
void enableAsyncUsesPrimaryExecutorWhenModeIsForceAndHasCustomTaskExecutor() {
this.contextRunner
.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-",
"spring.task.execution.mode=force")
.withBean("customTaskExecutor", Executor.class, () -> createCustomAsyncExecutor("custom-task-"),
(bd) -> bd.setPrimary(true))
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
.run((context) -> {
assertThat(context).hasSingleBean(AsyncConfigurer.class);
assertThat(context.getBeansOfType(Executor.class)).hasSize(2);
TestBean bean = context.getBean(TestBean.class);
String text = bean.echo("something").get();
assertThat(text).contains("custom-task-").contains("something"); //failed
});
} My intention was to preserve the existing logic and rely on the In any case, I have shared all my concerns and received feedback, and it appears that you are either already aware of them or have an alternative approach in mind. Thank you for your time. |
Signed-off-by: Dmytro Nosan <dimanosan@gmail.com>
This is the advertized behaviour, see the note in the reference documentation. The idea of force is really to configure an executor even though one is present. This wasn't possible before and come with its own semantic. If you use force, it doesn't matter what arrangement you have, we'll pick the auto-configured executor. Your only way out is to define your own
Not all of them, so thanks for that. Based on your feedback we made some adjustments both in the code and in the release notes. From an upgrade perspective, only the |
There appears to be a bug in TaskExecutorConfiguration when creating an AsyncTaskExecutor bean, the condition is:
instead of on AsyncTaskExecutor.
The result is, if an app creates a bean of type Executor (such as a ScheduledExecutorService), this results in Spring’s default AsyncTaskExecutor beans to not be created. The application will fail to start because of the missing beans.
This is for apps trying to use the bean generated for the configuration with the Task Execution and Scheduling feature. If an app creates a bean of type Executor, the above bean never gets created.
The text was updated successfully, but these errors were encountered: