Repository navigation
GH-641 - Use dedicated task executor for @ApplicationModuleListeners. - #1900
Open
dlwhdgus0810 wants to merge 1 commit into
Open
dlwhdgus0810 wants to merge 1 commit into
dlwhdgus0810 wants to merge 1 commit into
Conversation
…oduleListeners. @ApplicationModuleListener now runs on the executor bean named applicationModuleListenerTaskExecutor if present, and on the default async executor otherwise. The fallback keeps setups without that bean working (plain Spring applications, applications only using spring-modulith-events-api), in which a plain bean name qualifier would fail with a NoSuchBeanDefinitionException on listener invocation. A runtime hint allows evaluating the qualifier expression in native images. EventPublicationAutoConfiguration registers the executor through Spring Boot's ThreadPoolTaskExecutorBuilder, or SimpleAsyncTaskExecutorBuilder if virtual threads are enabled, so that the spring.task.execution.* properties and task executor customizers apply to it. It backs off if a bean with that name is declared and is not a default candidate, so that by-type lookups keep resolving the application's default executor. Signed-off-by: Hyun Lee <dlwhdugs4147@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #641.
@ApplicationModuleListenermethods now run on an executor bean namedapplicationModuleListenerTaskExecutor(also available asApplicationModuleListener.TASK_EXECUTOR_BEAN_NAME), as outlined in the issue.The executor
EventPublicationAutoConfigurationregisters it the same way Boot registersapplicationTaskExecutor: a@LazyThreadPoolTaskExecutorfromThreadPoolTaskExecutorBuilder, or aSimpleAsyncTaskExecutorfromSimpleAsyncTaskExecutorBuilderwhen virtual threads are enabled (@ConditionalOnThreading). This meansspring.task.execution.*and anyThreadPoolTaskExecutorCustomizer/SimpleAsyncTaskExecutorCustomizerbeans apply to it. That includes theContextPropagatingTaskDecoratorcustomizers of the observability module, so the observability module needs no changes.@ConditionalOnMissingBean(name = …). You can replace it by declaring a bean with that name, or customize it on its own with aBeanPostProcessor. The auto-configuration now runs afterTaskExecutionAutoConfigurationand only registers the executor if the builder bean is present.defaultCandidate = false. Without that, a secondTaskExecutorwould break by-type lookups and injection with aNoUniqueBeanDefinitionException. It would also makeScenarioCustomizer.forwardExecutorService(…), which relies ongetIfUnique(), stop forwardingapplicationTaskExecutor. Boot's bean conditions ignore non-default candidates too, soapplicationTaskExecutoris still created.When the bean is absent
@Async("applicationModuleListenerTaskExecutor")alone fails withNoSuchBeanDefinitionException: No bean named 'applicationModuleListenerTaskExecutor' availablewhenever that bean doesn't exist. That covers plain Spring setups (e.g.@EnablePersistentDomainEvents), Boot apps that only usespring-modulith-events-apitogether with@EnableAsync, and any context without this auto-configuration. The listener then never runs, and the only sign is the after-completion error in the log. So the qualifier is an expression that resolves to the bean name if such a bean exists, and to an empty qualifier otherwise. An empty qualifier means the default async executor, which is today's behavior:BeanExpressionContext.containsObject(String), the method the expression calls, gets a reflection hint through a newRuntimeHintsRegistrarinspring-modulith-events-api, so native images can evaluate it too. If you'd rather treat those setups as unsupported, going back to the plain name is a one-line change.One caveat of the expression: an application that disables SpEL with
spring.spel.ignore=truewould get the expression text as the qualifier, which isn't a bean name, so its listeners would fail to run whether or not the bean exists. With the plain name, that application would only need the bean to be present.Behavior change
In Boot apps with
spring-modulith-events-core, listeners no longer run on the default async executor, whether that isapplicationTaskExecutor, anAsyncConfigurer's executor or a user-declaredExecutor. They run on the new executor instead. To keep using your own executor, give it the nameapplicationModuleListenerTaskExecutoras well.Not included
ModuleObservabilityAutoConfiguration) would stop propagating context onapplicationTaskExecutor, so I'd rather do it separately if you want it.spring.task.execution.thread-name-prefixasapplicationTaskExecutor, so both pools name their threadstask-N. I can give it a prefix of its own if you prefer.Docs
New "Listener Executor" section right after the
@ApplicationModuleListenerdocs inevents.adoc, plus a note in the annotation's Javadoc.Tests
EventPublicationAutoConfigurationIntegrationTests:TaskExecutorlookup still returnsapplicationTaskExecutorThreadPoolTaskExecutorCustomizers apply@ApplicationModuleListeneractually runs on the dedicated executor: 1 task there, 0 onapplicationTaskExecutorApplicationModuleListenerIntegrationTests(plain Spring, no Boot): the listener uses a bean with that name if present and falls back to the default executor otherwise.ApplicationModuleListenerUnitTests: the hint is picked up throughaot.factories.I checked that the new tests fail without the corresponding change:
@Async: the dedicated-executor tests failNoSuchBeanDefinitionExceptionabovedefaultCandidate = false: the by-type lookup fails withNoUniqueBeanDefinitionExceptionaot.factories: the hint test failsCommands I ran locally:
./mvnw -B -pl spring-modulith-events/spring-modulith-events-api,spring-modulith-events/spring-modulith-events-core,spring-modulith-events/spring-modulith-events-tests,spring-modulith-observability/spring-modulith-observability-core,spring-modulith-examples/spring-modulith-example-full -am verify→ BUILD SUCCESS (504 tests, 0 failures; 7 skipped, including the JDK 21-only test)../mvnw -B -pl spring-modulith-events/spring-modulith-events-api,spring-modulith-events/spring-modulith-events-core verify→ BUILD SUCCESS (virtual threads test included)../mvnw -B -Pnullaway -pl spring-modulith-events/spring-modulith-events-api,spring-modulith-events/spring-modulith-events-core -DskipTests compile→ BUILD SUCCESS.I didn't build a native image. The hint is only covered by the unit test above.