Skip to content

Commit

Permalink
Revert use of Map::computeIfAbsent in thread and tx scopes
Browse files Browse the repository at this point in the history
Issues gh-25038 and gh-25618 collectively introduced a regression for
thread-scoped and transaction-scoped beans.

For example, given a thread-scoped bean X that depends on another
thread-scoped bean Y, if the names of the beans (when used as map keys)
end up in the same bucket within a ConcurrentHashMap AND an attempt is
made to retrieve bean X from the ApplicationContext prior to retrieving
bean Y, then the use of Map::computeIfAbsent in SimpleThreadScope
results in recursive access to the same internal bucket in the map.

On Java 8, that scenario simply hangs. On Java 9 and higher,
ConcurrentHashMap throws an IllegalStateException pointing out that a
"Recursive update" was attempted.

In light of these findings, we are reverting the changes made to
SimpleThreadScope and SimpleTransactionScope in commits 50a4fda and
148dc95.

Closes gh-25801
  • Loading branch information
sbrannen committed Sep 25, 2020
1 parent a532c52 commit f5d36aa
Show file tree
Hide file tree
Showing 4 changed files with 34 additions and 12 deletions.
Expand Up @@ -16,8 +16,8 @@

package org.springframework.context.support;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand Down Expand Up @@ -59,15 +59,22 @@ public class SimpleThreadScope implements Scope {
new NamedThreadLocal<Map<String, Object>>("SimpleThreadScope") {
@Override
protected Map<String, Object> initialValue() {
return new ConcurrentHashMap<>();
return new HashMap<>();
}
};


@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = this.threadScope.get();
return scope.computeIfAbsent(name, k -> objectFactory.getObject());
// NOTE: Do NOT modify the following to use Map::computeIfAbsent. For details,
// see https://github.com/spring-projects/spring-framework/issues/25801.
Object scopedObject = scope.get(name);
if (scopedObject == null) {
scopedObject = objectFactory.getObject();
scope.put(name, scopedObject);
}
return scopedObject;
}

@Override
Expand Down
Expand Up @@ -38,7 +38,7 @@ class SimpleThreadScopeTests {

@Test
void getFromScope() throws Exception {
String name = "threadScopedObject";
String name = "removeNodeStatusScreen";
TestBean bean = this.applicationContext.getBean(name, TestBean.class);
assertThat(bean).isNotNull();
assertThat(this.applicationContext.getBean(name)).isSameAs(bean);
Expand All @@ -50,8 +50,8 @@ void getFromScope() throws Exception {
void getMultipleInstances() throws Exception {
// Arrange
TestBean[] beans = new TestBean[2];
Thread thread1 = new Thread(() -> beans[0] = applicationContext.getBean("threadScopedObject", TestBean.class));
Thread thread2 = new Thread(() -> beans[1] = applicationContext.getBean("threadScopedObject", TestBean.class));
Thread thread1 = new Thread(() -> beans[0] = applicationContext.getBean("removeNodeStatusScreen", TestBean.class));
Thread thread2 = new Thread(() -> beans[1] = applicationContext.getBean("removeNodeStatusScreen", TestBean.class));
// Act
thread1.start();
thread2.start();
Expand Down
Expand Up @@ -12,10 +12,18 @@
</property>
</bean>

<bean id="threadScopedObject" class="org.springframework.beans.testfixture.beans.TestBean" scope="thread">
<property name="spouse" ref="threadScopedObject2" />
<!--
NOTE: The bean names removeNodeStatusScreen and removeNodeStatusPresenter are seemingly
quite odd for TestBean instances; however, these have been chosen due to the fact that
they end up in the same bucket within a HashMap/ConcurrentHashMap initialized with the
default initial capacity.
For details see: https://github.com/spring-projects/spring-framework/issues/25801
-->
<bean id="removeNodeStatusScreen" class="org.springframework.beans.testfixture.beans.TestBean" scope="thread">
<property name="spouse" ref="removeNodeStatusPresenter" />
</bean>

<bean id="threadScopedObject2" class="org.springframework.beans.testfixture.beans.TestBean" scope="thread" />
<bean id="removeNodeStatusPresenter" class="org.springframework.beans.testfixture.beans.TestBean" scope="thread" />

</beans>
Expand Up @@ -16,9 +16,9 @@

package org.springframework.transaction.support;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
Expand Down Expand Up @@ -50,7 +50,14 @@ public Object get(String name, ObjectFactory<?> objectFactory) {
TransactionSynchronizationManager.registerSynchronization(new CleanupSynchronization(scopedObjects));
TransactionSynchronizationManager.bindResource(this, scopedObjects);
}
return scopedObjects.scopedInstances.computeIfAbsent(name, k -> objectFactory.getObject());
// NOTE: Do NOT modify the following to use Map::computeIfAbsent. For details,
// see https://github.com/spring-projects/spring-framework/issues/25801.
Object scopedObject = scopedObjects.scopedInstances.get(name);
if (scopedObject == null) {
scopedObject = objectFactory.getObject();
scopedObjects.scopedInstances.put(name, scopedObject);
}
return scopedObject;
}

@Override
Expand Down Expand Up @@ -92,7 +99,7 @@ public String getConversationId() {
*/
static class ScopedObjectsHolder {

final Map<String, Object> scopedInstances = new ConcurrentHashMap<>();
final Map<String, Object> scopedInstances = new HashMap<>();

final Map<String, Runnable> destructionCallbacks = new LinkedHashMap<>();
}
Expand Down

0 comments on commit f5d36aa

Please sign in to comment.