Skip to content

Commit

Permalink
Wait for lenient bean creation in locked thread when necessary
Browse files Browse the repository at this point in the history
Closes gh-34349
  • Loading branch information
jhoeller committed Feb 12, 2025
1 parent 056757b commit b336bbe
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
Expand Down Expand Up @@ -100,6 +101,15 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
/** Names of beans currently excluded from in creation checks. */
private final Set<String> inCreationCheckExclusions = ConcurrentHashMap.newKeySet(16);

/** Specific lock for lenient creation tracking. */
private final Lock lenientCreationLock = new ReentrantLock();

/** Specific lock condition for lenient creation tracking. */
private final Condition lenientCreationFinished = this.lenientCreationLock.newCondition();

/** Names of beans that are currently in lenient creation. */
private final Set<String> singletonsInLenientCreation = new HashSet<>();

/** Flag that indicates whether we're currently within destroySingletons. */
private volatile boolean singletonsCurrentlyInDestruction = false;

Expand Down Expand Up @@ -243,6 +253,7 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock();
boolean acquireLock = !Boolean.FALSE.equals(lockFlag);
boolean locked = (acquireLock && this.singletonLock.tryLock());
boolean lenient = false;
try {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
Expand All @@ -257,6 +268,14 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Thread.currentThread().getName() + "\" while other thread holds " +
"singleton lock for other beans " + this.singletonsCurrentlyInCreation);
}
lenient = true;
this.lenientCreationLock.lock();
try {
this.singletonsInLenientCreation.add(beanName);
}
finally {
this.lenientCreationLock.unlock();
}
}
else {
// No specific locking indication (outside a coordinated bootstrap) and
Expand Down Expand Up @@ -285,7 +304,24 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
}
catch (BeanCurrentlyInCreationException ex) {
if (locked) {
throw ex;
this.lenientCreationLock.lock();
try {
while ((singletonObject = this.singletonObjects.get(beanName)) == null) {
if (!this.singletonsInLenientCreation.contains(beanName)) {
throw ex;
}
try {
this.lenientCreationFinished.await();
}
catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
return singletonObject;
}
finally {
this.lenientCreationLock.unlock();
}
}
// Try late locking for waiting on specific bean to be finished.
this.singletonLock.lock();
Expand Down Expand Up @@ -339,6 +375,16 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
if (locked) {
this.singletonLock.unlock();
}
if (lenient) {
this.lenientCreationLock.lock();
try {
this.singletonsInLenientCreation.remove(beanName);
this.lenientCreationFinished.signalAll();
}
finally {
this.lenientCreationLock.unlock();
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;

import org.springframework.beans.factory.BeanCurrentlyInCreationException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.testfixture.EnabledForTestGroups;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.springframework.context.annotation.Bean.Bootstrap.BACKGROUND;
import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING;

Expand All @@ -42,8 +40,19 @@ class BackgroundBootstrapTests {
void bootstrapWithUnmanagedThread() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadBeanConfig.class);
ctx.getBean("testBean1", TestBean.class);
assertThatExceptionOfType(BeanCurrentlyInCreationException.class).isThrownBy( // late - not during refresh
() -> ctx.getBean("testBean2", TestBean.class));
ctx.getBean("testBean2", TestBean.class);
ctx.close();
}

@Test
@Timeout(5)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithUnmanagedThreads() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadsBeanConfig.class);
ctx.getBean("testBean1", TestBean.class);
ctx.getBean("testBean2", TestBean.class);
ctx.getBean("testBean3", TestBean.class);
ctx.getBean("testBean4", TestBean.class);
ctx.close();
}

Expand All @@ -55,6 +64,7 @@ void bootstrapWithCustomExecutor() {
ctx.getBean("testBean1", TestBean.class);
ctx.getBean("testBean2", TestBean.class);
ctx.getBean("testBean3", TestBean.class);
ctx.getBean("testBean4", TestBean.class);
ctx.close();
}

Expand Down Expand Up @@ -87,6 +97,45 @@ public TestBean testBean2() {
}


@Configuration(proxyBeanMethods = false)
static class UnmanagedThreadsBeanConfig {

@Bean
public TestBean testBean1(ObjectProvider<TestBean> testBean3, ObjectProvider<TestBean> testBean4) {
new Thread(testBean3::getObject).start();
new Thread(testBean4::getObject).start();
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
return new TestBean();
}

@Bean
public TestBean testBean2(TestBean testBean4) {
return new TestBean(testBean4);
}

@Bean
public TestBean testBean3(TestBean testBean4) {
return new TestBean(testBean4);
}

@Bean
public TestBean testBean4() {
try {
Thread.sleep(2000);
}
catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
return new TestBean();
}
}


@Configuration(proxyBeanMethods = false)
static class CustomExecutorBeanConfig {

Expand Down Expand Up @@ -117,8 +166,8 @@ public TestBean testBean3() {
}

@Bean
public String dependent(@Lazy TestBean testBean1, @Lazy TestBean testBean2, @Lazy TestBean testBean3) {
return "";
public TestBean testBean4(@Lazy TestBean testBean1, @Lazy TestBean testBean2, @Lazy TestBean testBean3) {
return new TestBean();
}
}

Expand Down

0 comments on commit b336bbe

Please sign in to comment.