Skip to content

Commit 2dfa578

Browse files
committed
Fixed random "recursive update during ServiceLoading" exception
1 parent a551446 commit 2dfa578

File tree

14 files changed

+339
-44
lines changed

14 files changed

+339
-44
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# 2.4.1
2+
* Fixed random "recursive update during ServiceLoading" exception #342
3+
14
# 2.4.0
25
* ``oidc-server-mock``
36
* Improved extensibility by creating abstract base classes

base-demo/src/test/java/software/xdev/tci/factory/prestart/PreStartDemoTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import software.xdev.tci.dummyinfra.factory.DummyTCIFactory;
1010
import software.xdev.tci.factory.prestart.config.PreStartConfig;
1111
import software.xdev.tci.factory.registry.TCIFactoryRegistry;
12-
import software.xdev.tci.serviceloading.TCIServiceLoader;
12+
import software.xdev.tci.serviceloading.TCIServiceLoaderHolder;
1313

1414

1515
class PreStartDemoTest
@@ -23,7 +23,8 @@ class PreStartDemoTest
2323
static void beforeAll()
2424
{
2525
// Force enable PreStarting
26-
TCIServiceLoader.instance().forceOverwrite(PreStartConfig.class, new PreStartConfig()
26+
TCIServiceLoaderHolder.instance().forceOverwrite(
27+
PreStartConfig.class, new PreStartConfig()
2728
{
2829
@Override
2930
public boolean enabled()

base/src/main/java/software/xdev/tci/envperf/EnvironmentPerformance.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
package software.xdev.tci.envperf;
1717

1818
import software.xdev.tci.envperf.impl.TCIEnvironmentPerformance;
19-
import software.xdev.tci.serviceloading.TCIServiceLoader;
19+
import software.xdev.tci.serviceloading.TCIServiceLoaderHolder;
2020

2121

2222
/**
@@ -34,7 +34,7 @@ public static int cpuSlownessFactor()
3434

3535
public static TCIEnvironmentPerformance impl()
3636
{
37-
return TCIServiceLoader.instance().service(TCIEnvironmentPerformance.class);
37+
return TCIServiceLoaderHolder.instance().service(TCIEnvironmentPerformance.class);
3838
}
3939

4040
private EnvironmentPerformance()

base/src/main/java/software/xdev/tci/factory/prestart/config/PreStartConfig.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package software.xdev.tci.factory.prestart.config;
1717

18-
import software.xdev.tci.serviceloading.TCIServiceLoader;
18+
import software.xdev.tci.serviceloading.TCIServiceLoaderHolder;
1919

2020

2121
public interface PreStartConfig
@@ -91,6 +91,6 @@ default boolean detectEndingTests()
9191

9292
static PreStartConfig instance()
9393
{
94-
return TCIServiceLoader.instance().service(PreStartConfig.class);
94+
return TCIServiceLoaderHolder.instance().service(PreStartConfig.class);
9595
}
9696
}

base/src/main/java/software/xdev/tci/factory/prestart/coordinator/GlobalPreStartCoordinator.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
package software.xdev.tci.factory.prestart.coordinator;
1717

1818
import software.xdev.tci.factory.prestart.PreStartableTCIFactory;
19-
import software.xdev.tci.serviceloading.TCIServiceLoader;
19+
import software.xdev.tci.serviceloading.TCIServiceLoaderHolder;
2020

2121

2222
public interface GlobalPreStartCoordinator extends AutoCloseable
@@ -30,11 +30,11 @@ public interface GlobalPreStartCoordinator extends AutoCloseable
3030

3131
static GlobalPreStartCoordinator instance()
3232
{
33-
return TCIServiceLoader.instance().service(GlobalPreStartCoordinator.class);
33+
return TCIServiceLoaderHolder.instance().service(GlobalPreStartCoordinator.class);
3434
}
3535

3636
static boolean isPresent()
3737
{
38-
return TCIServiceLoader.instance().isLoaded(GlobalPreStartCoordinator.class);
38+
return TCIServiceLoaderHolder.instance().isLoaded(GlobalPreStartCoordinator.class);
3939
}
4040
}

base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/LoadMonitor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import java.util.OptionalDouble;
1919

20-
import software.xdev.tci.serviceloading.TCIServiceLoader;
20+
import software.xdev.tci.serviceloading.TCIServiceLoaderHolder;
2121

2222

2323
/**
@@ -35,6 +35,6 @@ public interface LoadMonitor
3535

3636
static LoadMonitor instance()
3737
{
38-
return TCIServiceLoader.instance().service(LoadMonitor.class);
38+
return TCIServiceLoaderHolder.instance().service(LoadMonitor.class);
3939
}
4040
}

base/src/main/java/software/xdev/tci/factory/registry/TCIFactoryRegistry.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import software.xdev.tci.TCI;
2323
import software.xdev.tci.factory.TCIFactory;
24-
import software.xdev.tci.serviceloading.TCIServiceLoader;
24+
import software.xdev.tci.serviceloading.TCIServiceLoaderHolder;
2525

2626

2727
/**
@@ -45,6 +45,6 @@ public interface TCIFactoryRegistry
4545

4646
static TCIFactoryRegistry instance()
4747
{
48-
return TCIServiceLoader.instance().service(TCIFactoryRegistry.class);
48+
return TCIServiceLoaderHolder.instance().service(TCIFactoryRegistry.class);
4949
}
5050
}

base/src/main/java/software/xdev/tci/leakdetection/TCILeakAgent.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
import software.xdev.tci.factory.TCIFactory;
3838
import software.xdev.tci.factory.registry.TCIFactoryRegistry;
3939
import software.xdev.tci.leakdetection.config.LeakDetectionConfig;
40-
import software.xdev.tci.serviceloading.TCIServiceLoader;
40+
import software.xdev.tci.serviceloading.TCIServiceLoaderHolder;
4141

4242

4343
/**
@@ -58,7 +58,7 @@ public class TCILeakAgent implements TestExecutionListener
5858
@Override
5959
public void testPlanExecutionStarted(final TestPlan testPlan)
6060
{
61-
this.config = TCIServiceLoader.instance().service(LeakDetectionConfig.class);
61+
this.config = TCIServiceLoaderHolder.instance().service(LeakDetectionConfig.class);
6262
if(!this.config.enabled())
6363
{
6464
return;

base/src/main/java/software/xdev/tci/serviceloading/TCIProviderPriority.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323

2424
/**
25-
* Priority of a provider for {@link TCIServiceLoader}.
25+
* Priority of a provider for {@link TCIServiceLoaderHolder}.
2626
*/
2727
@Retention(RetentionPolicy.RUNTIME)
2828
@Target(ElementType.TYPE)

base/src/main/java/software/xdev/tci/serviceloading/TCIServiceLoader.java

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,50 +16,115 @@
1616
package software.xdev.tci.serviceloading;
1717

1818
import java.util.Comparator;
19+
import java.util.LinkedHashSet;
1920
import java.util.Map;
2021
import java.util.Optional;
2122
import java.util.ServiceLoader;
2223
import java.util.concurrent.ConcurrentHashMap;
24+
import java.util.concurrent.locks.ReentrantLock;
25+
import java.util.stream.Collectors;
2326

2427

2528
/**
2629
* Central point for service loading
2730
*/
28-
@SuppressWarnings("java:S6548")
29-
public final class TCIServiceLoader
31+
@SuppressWarnings({"java:S6548", "java:S2789"}) // Don't force us to write our own Optional!?!
32+
public class TCIServiceLoader
3033
{
31-
private static final TCIServiceLoader INSTANCE = new TCIServiceLoader();
34+
protected final Object globalInitServiceLockHandlerLock = new Object();
35+
protected final InheritableThreadLocal<LinkedHashSet<Class<?>>> tlDetectRecursiveInitServices =
36+
new InheritableThreadLocal<>();
37+
protected final Map<Class<?>, ReentrantLock> servicesLoadingSyncLocks = new ConcurrentHashMap<>();
38+
protected final Map<Class<?>, Optional<?>> loadedServices = new ConcurrentHashMap<>();
3239

33-
public static TCIServiceLoader instance()
40+
public <T> T service(final Class<T> clazz)
3441
{
35-
return INSTANCE;
42+
// Do not use computeIfAbsent here. This might cause "recursive updates" exceptions.
43+
// These are usually not recursive, there is just a call here doing another call with a different type
44+
// Recursive updates will be detected by initService
45+
46+
if(!this.loadedServices.containsKey(clazz))
47+
{
48+
this.initService(clazz);
49+
}
50+
51+
return this.loadedServices.get(clazz)
52+
.map(clazz::cast)
53+
.orElse(null);
3654
}
3755

38-
private final Map<Class<?>, Object> loadedServices = new ConcurrentHashMap<>();
39-
40-
private TCIServiceLoader()
56+
/**
57+
* @implNote This method uses best effort thread safety. It WILL fail when another Thread that didn't inherit from
58+
* the current Thread is causing a deadlock, for example <code>ForkJoinPool.commonPool</code>
59+
*/
60+
protected <T> void initService(final Class<T> clazz)
4161
{
62+
final LinkedHashSet<Class<?>> detectRecursiveInitServices;
63+
final ReentrantLock lock;
64+
synchronized(this.globalInitServiceLockHandlerLock)
65+
{
66+
detectRecursiveInitServices = Optional.ofNullable(this.tlDetectRecursiveInitServices.get())
67+
.map(LinkedHashSet::new)
68+
.orElseGet(LinkedHashSet::new);
69+
70+
if(detectRecursiveInitServices.contains(clazz))
71+
{
72+
throw new IllegalStateException("Detected recursive initialization on "
73+
+ clazz
74+
+ "; chain trying class creation: "
75+
+ detectRecursiveInitServices.stream()
76+
.map(Class::getName)
77+
.collect(Collectors.joining(" -> ")));
78+
}
79+
80+
detectRecursiveInitServices.add(clazz);
81+
this.tlDetectRecursiveInitServices.set(detectRecursiveInitServices);
82+
83+
lock = this.servicesLoadingSyncLocks.computeIfAbsent(
84+
clazz,
85+
ignored -> new ReentrantLock());
86+
lock.lock();
87+
}
88+
89+
try
90+
{
91+
// Already initialized?
92+
if(this.loadedServices.containsKey(clazz))
93+
{
94+
return;
95+
}
96+
97+
this.loadedServices.put(clazz, this.loadService(clazz));
98+
}
99+
finally
100+
{
101+
synchronized(this.globalInitServiceLockHandlerLock)
102+
{
103+
detectRecursiveInitServices.remove(clazz);
104+
this.tlDetectRecursiveInitServices.set(detectRecursiveInitServices);
105+
106+
this.servicesLoadingSyncLocks.remove(clazz);
107+
lock.unlock();
108+
}
109+
}
42110
}
43111

44-
@SuppressWarnings("unchecked")
45-
public <T> T service(final Class<T> clazz)
112+
protected <T> Optional<T> loadService(final Class<T> clazz)
46113
{
47-
return (T)this.loadedServices.computeIfAbsent(
48-
clazz,
49-
c -> ServiceLoader.load(clazz)
50-
.stream()
51-
// Get by highest priority
52-
.max(Comparator.comparingInt(p ->
53-
Optional.ofNullable(p.type().getAnnotation(TCIProviderPriority.class))
54-
.map(TCIProviderPriority::value)
55-
.orElse(TCIProviderPriority.DEFAULT_PRIORITY)))
56-
.map(ServiceLoader.Provider::get)
57-
.orElse(null));
114+
return ServiceLoader.load(clazz)
115+
.stream()
116+
// Get by highest priority
117+
.max(Comparator.comparingInt(p ->
118+
Optional.ofNullable(p.type().getAnnotation(TCIProviderPriority.class))
119+
.map(TCIProviderPriority::value)
120+
.orElse(TCIProviderPriority.DEFAULT_PRIORITY)))
121+
.map(ServiceLoader.Provider::get);
58122
}
59123

60124
public boolean isLoaded(final Class<?> clazz)
61125
{
62-
return this.loadedServices.get(clazz) != null;
126+
final Optional<?> optImpl = this.loadedServices.get(clazz);
127+
return optImpl != null && optImpl.isPresent();
63128
}
64129

65130
/**
@@ -68,8 +133,8 @@ public boolean isLoaded(final Class<?> clazz)
68133
* WARNING: Usage not recommended. Should only be used as last resort!
69134
* </p>
70135
*/
71-
public Object forceOverwrite(final Class<?> clazz, final Object value)
136+
public void forceOverwrite(final Class<?> clazz, final Object value)
72137
{
73-
return this.loadedServices.put(clazz, value);
138+
this.loadedServices.put(clazz, Optional.ofNullable(value));
74139
}
75140
}

0 commit comments

Comments
 (0)