Description
Hi team,
A user reported an issue on my project (jdbc-observations/datasource-micrometer#54) with a sample repro where the application hangs during startup. Upon debugging, I traced this change (384dc2a) for 6.2.1
caused the issue.
Conditions:
- Enable background
EntityManager
creation- With Spring Boot,
spring.data.jpa.repositories.bootstrap-mode
is set tolazy
ordeferred
(handled byJpaRepositoriesAutoConfiguration
).
- With Spring Boot,
- Lazy
BeanFactory
access in beans associated withEntityManager
- A dependent bean (e.g., a bean used by
DataSource
) accessed lazily viaObjectProvider
duringEntityManagerFactory
creation.
- A dependent bean (e.g., a bean used by
Problem:
A deadlock occurs between the main thread and the background thread creating the EntityManagerFactory
.
- The main thread holds the
singletonLock
while waiting for theEntityManagerFactory
to be created. - The
EntityManagerFactory
is being created in a separate thread, and a dependent bean in that process tries to lazily access theBeanFactory
viaObjectProvider
. - The
ObjectProvider
access in the background thread tries to acquire thesingletonLock
, leading to a deadlock.
Detailed Code Flow:
-
Background Thread:
- With background bootstrap enabled,
LocalContainerEntityManagerFactoryBean
initiates the creation of theEntityManagerFactory
(NativeEntityManagerFactory
) in a separate thread (via a task submission, holding aFuture
for the result).
- With background bootstrap enabled,
-
Main Thread:
- While the background task runs, the main thread continues creating other beans, including JPA-related factory beans such as
JpaMetamodelMappingContextFactoryBean
. JpaMetamodelMappingContextFactoryBean
(a subclass ofAbstractFactoryBean
) invokes itsafterPropertiesSet()
method to create a bean whilesingletonLock
is acquired.- During the creation of the
JpaMetamodelMappingContext
, a call togetMetamodels()
is made, which invokes a proxy and callsAbstractEntityManagerFactoryBean#getNativeEntityManagerFactory()
. This method calls a blocking call to theFuture
, waiting for the background thread to completeEntityManagerFactory
creation.
- While the background task runs, the main thread continues creating other beans, including JPA-related factory beans such as
-
Background Thread (Continued):
- During
EntityManagerFactory
creation, JDBC-related beans (e.g.,DataSource
) are instantiated. - With
datasource-micrometer
, theDataSource
bean references other beans, one of which lazily resolves anobservationRegistry
viaObjectProvider
. - A database query is triggered as part of the Hibernate entity manager creation process(retrieving the database metadata), causing the
ObjectProvider
to attempt to access thebeanFactory
. It tries to acquire thesingletonLock
, which is already held by the main thread, resulting in a deadlock.
- During
This is a repro sample code capturing the above scenario concept:
Simple Repro Code
class MyTest {
private static final Logger log = LoggerFactory.getLogger(MyTest.class);
@Test
void test() {
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
Object foo = context.getBean("foo");
assertThat(foo).isInstanceOf(Foo.class);
}
static class Foo {
}
static class Bar {
}
// extend "AbstractFactoryBean" to simulate "JpaMetamodelMappingContextFactoryBean"
static class FooFactory extends AbstractFactoryBean<Foo> {
private ObjectProvider<Bar> barProvider;
public FooFactory(ObjectProvider<Bar> barProvider) {
this.barProvider = barProvider;
}
@Override
protected Foo createInstance() throws Exception {
// retrieve a bean in a separate thread. emulating "EntityManagerFactory" creation
// in "AbstractEntityManagerFactoryBean#afterPropertiesSet"
Future<Bar> future = Executors.newSingleThreadExecutor().submit(() -> {
log.info("getting bar before");
Bar bar = this.barProvider.getObject(); // <== deadlock here
// against "singletonLock"("DefaultSingletonBeanRegistry") used by "AbstractAutowireCapableBeanFactory.getSingletonFactoryBeanForTypeCheck"
log.info("getting bar after");
return bar;
});
Bar bar = future.get();
log.info("bar={}", bar);
return new Foo();
}
@Override
public Class<?> getObjectType() {
return Foo.class;
}
}
@Configuration(proxyBeanMethods = false)
@Import(FooFactoryRegistrar.class)
static class MyConfiguration {
@Bean
Bar bar() {
return new Bar();
}
// Alternate to bean definition registrar
// @Bean("foo")
// static FooFactory fooFactory(ObjectProvider<Bar> barProvider) {
// return new FooFactory(barProvider);
// }
}
// simulating "JpaAuditingRegistrar" which registers "JpaMetamodelMappingContextFactoryBean"
static class FooFactoryRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
registry.registerBeanDefinition("foo", new RootBeanDefinition(FooFactory.class));
}
}
}
I'm not sure if this is to be called a regression or a new requirement for 6.2.1
.
If it is a new behavior, the limitation would be that any EntityManager
infrastructure beans (such as beans related to DataSource
) cannot have lazy access to the BeanFactory
when background EntityManager
bootstrap is enabled due to the use of a separate thread.