springboot version:
'org.springframework.boot:spring-boot-dependencies:2.1.6.RELEASE'
Junit5:
testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2'
testImplementation 'org.junit.platform:junit-platform-runner:1.4.2'
testImplementation 'org.junit.platform:junit-platform-launcher:1.4.2'
Scenario:
we met the same ConcurrentModificationException issue when we manually run multiple SpringApplications in Junit5 parallel mode.
Issue:
below is exception stack:
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at ch.qos.logback.classic.LoggerContext.fireOnLevelChange(LoggerContext.java:317)
at ch.qos.logback.classic.Logger.setLevel(Logger.java:173)
at org.springframework.boot.logging.logback.LogbackConfigurator.root(LogbackConfigurator.java:101)
at org.springframework.boot.logging.logback.DefaultLogbackConfiguration.apply(DefaultLogbackConfiguration.java:95)
at org.springframework.boot.logging.logback.LogbackLoggingSystem.loadDefaults(LogbackLoggingSystem.java:141)
at org.springframework.boot.logging.AbstractLoggingSystem.initializeWithConventions(AbstractLoggingSystem.java:85)
at org.springframework.boot.logging.AbstractLoggingSystem.initialize(AbstractLoggingSystem.java:60)
at org.springframework.boot.logging.logback.LogbackLoggingSystem.initialize(LogbackLoggingSystem.java:117)
at org.springframework.boot.context.logging.LoggingApplicationListener.initializeSystem(LoggingApplicationListener.java:293)
at org.springframework.boot.context.logging.LoggingApplicationListener.initialize(LoggingApplicationListener.java:266)
at org.springframework.boot.context.logging.LoggingApplicationListener.onApplicationEnvironmentPreparedEvent(LoggingApplicationListener.java:229)
at org.springframework.boot.context.logging.LoggingApplicationListener.onApplicationEvent(LoggingApplicationListener.java:202)
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:127)
at org.springframework.boot.context.event.EventPublishingRunListener.environmentPrepared(EventPublishingRunListener.java:75)
at org.springframework.boot.SpringApplicationRunListeners.environmentPrepared(SpringApplicationRunListeners.java:54)
at org.springframework.boot.SpringApplication.prepareEnvironment(SpringApplication.java:347)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:306)
at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:139)
at org.springframework.cloud.bootstrap.BootstrapApplicationListener.bootstrapServiceContext(BootstrapApplicationListener.java:203)
at org.springframework.cloud.bootstrap.BootstrapApplicationListener.onApplicationEvent(BootstrapApplicationListener.java:114)
at org.springframework.cloud.bootstrap.BootstrapApplicationListener.onApplicationEvent(BootstrapApplicationListener.java:71)
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:127)
at org.springframework.boot.context.event.EventPublishingRunListener.environmentPrepared(EventPublishingRunListener.java:75)
at org.springframework.boot.SpringApplicationRunListeners.environmentPrepared(SpringApplicationRunListeners.java:54)
at org.springframework.boot.SpringApplication.prepareEnvironment(SpringApplication.java:347)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:306)
Comment From: wilkinsona
Thanks for the report. Unfortunately this is a known issue with Logback which means that concurrently adding or removing a LoggerContextListener and firing an event to the registered listeners will fail.
Spring Boot cannot restrict when listener registration changes or when events are fired to them so I do not believe it is possible for us to work around the problem entirely. That said, we may be able to do something to reduce the likelihood of it happening. I'll leave this issue open so that we can explore that possibility. In the meantime, you may need to use serial test execution. Alternatively you could try switching to Log4j2.
Comment From: wilkinsona
We've discussed this and unfortunately we don't think there's a reliable way in which we can fix this. Your best bet is to use one of the two alternatives I mentioned above.
Comment From: pschichtel
I also just switched to parallel test execution and noticed this issue.
I now overwrite the logback logging system with this one, to initialize logback just once.
private class SynchronizedSingletonLogbackLoggingSystem(private val classLoader: ClassLoader) : LoggingSystem() {
private val system = lock.withLock {
cachedLoggingSystem ?: LogbackLoggingSystem(classLoader).apply {
beforeInitialize()
cachedLoggingSystem = this
}
}
override fun beforeInitialize() = lock.withLock {
if (!initialized) {
initialized = true
system.beforeInitialize()
}
}
override fun initialize(initializationContext: LoggingInitializationContext?, configLocation: String?, logFile: LogFile?) = lock.withLock {
if (!initialized) {
initialized = true
system.initialize(initializationContext, configLocation, logFile)
}
}
override fun cleanUp() {
}
override fun getShutdownHandler(): Runnable? = null
override fun getSystemProperties(environment: ConfigurableEnvironment?): LoggingSystemProperties = lock.withLock {
system.getSystemProperties(environment)
}
override fun getSupportedLogLevels(): MutableSet<LogLevel> = lock.withLock {
system.supportedLogLevels
}
override fun setLogLevel(loggerName: String?, level: LogLevel?) = lock.withLock {
system.setLogLevel(loggerName, level)
}
override fun getLoggerConfigurations(): MutableList<LoggerConfiguration> = lock.withLock {
system.loggerConfigurations
}
override fun getLoggerConfiguration(loggerName: String?): LoggerConfiguration = lock.withLock {
system.getLoggerConfiguration(loggerName)
}
private companion object {
@Volatile
private var initialized = false
private var cachedLoggingSystem: LogbackLoggingSystem? = null
private val lock = ReentrantLock()
}
}