`ConcurrentModificationException` in `SchemaPropertyDeprecatingConverter.isDeprecated`, and `DEPRECATED_ANNOTATIONS` gains a duplicate on every context start
Les mainteneurs répondent en général sous 1 jour
Évaluation
- Difficulté
- 2/5
- Temps estimé
- 1-3 heures
- Accessibilité débutants
- 85/100
- Type d'issue
- Bug
- Clarté
- Clairement spécifiée
- Activité
- Active
- Domaine
- api, backend-api-design
Piste de recherche
Commencez par SchemaPropertyDeprecatingConverter.isDeprecated et la déclaration de DEPRECATED_ANNOTATIONS dans springdoc-openapi-starter-common ; comparez le comportement de leurs collections à celui des collections copy-on-write mentionnées dans l’issue. Utilisez la reproduction décrite dans l’issue pour vérifier l’enregistrement concurrent et les enregistrements répétés. Le travail est terminé lorsque les appels concurrents ne déclenchent pas d’exception et que les enregistrements répétés ne laissent que les deux types d’annotation attendus.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
Describe the bug
SchemaPropertyDeprecatingConverter keeps the deprecated annotation types in a JVM-wide static Collections.synchronizedList. Two problems in springdoc-openapi-starter-common:
isDeprecated(Method)callsDEPRECATED_ANNOTATIONS.stream()without holding the list's lock. A synchronized list only synchronizes its own methods, and the Javadoc ofCollections.synchronizedListrequires a manual lock for traversal by iterator, spliterator or stream. A concurrentaddmakes the stream throw aConcurrentModificationException.SpringDocKotlinConfiguration'sinitblock callsSpringDocUtils.getConfig().addDeprecatedType(Deprecated::class.java)on every application context start. The list never deduplicates, so it grows by one entry per context. EveryisDeprecatedcall (once per handler method per/v3/api-docsgeneration) then iterates the duplicates.
Together they make /v3/api-docs fail with a 500 whenever one context in the JVM starts while another generates its docs:
java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1703)
at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:147)
...
at java.base/java.util.stream.ReferencePipeline.anyMatch(ReferencePipeline.java:668)
at org.springdoc.core.converters.SchemaPropertyDeprecatingConverter.isDeprecated(SchemaPropertyDeprecatingConverter.java:87)
at org.springdoc.api.AbstractOpenApiResource...
We hit this in a Kotlin Spring Boot application's integration tests: JUnit 5 runs test classes in parallel (junit.jupiter.execution.parallel.mode.classes.default=concurrent), test classes with different configurations start their own contexts, and a test that requests /v3/api-docs fails intermittently. A single production context is not affected, because its one add happens before the server serves requests.
The other static lists of this kind were already fixed for the same reason (#1641): HIDDEN_REST_CONTROLLERS, ADDITIONAL_REST_CONTROLLERS and ANNOTATIONS_TO_IGNORE copy a snapshot under the lock, and the ConverterUtils, AbstractRequestService and PolymorphicModelConverter collections are CopyOnWriteArrayList/CopyOnWriteArraySet. DEPRECATED_ANNOTATIONS is the remaining synchronizedList that is streamed without the lock.
To Reproduce
- spring-boot: not needed for the reproduction; we see it with 4.1.1
- springdoc-openapi:
springdoc-openapi-starter-common3.2.0 (we first saw it on 3.0.3)
build.gradle.kts:
plugins { application }
repositories { mavenCentral() }
dependencies {
implementation("org.springdoc:springdoc-openapi-starter-common:3.2.0")
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.2.20")
implementation("org.springframework:spring-web:7.0.0")
}
application { mainClass = "Repro" }
src/main/java/Repro.java:
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.springdoc.core.configuration.SpringDocKotlinConfiguration;
import org.springdoc.core.converters.SchemaPropertyDeprecatingConverter;
public class Repro {
@Deprecated
public void endpoint() {
}
public static void main(String[] args) throws Exception {
// 1. Every Spring context start constructs SpringDocKotlinConfiguration, whose init block
// calls addDeprecatedType(kotlin.Deprecated) again: the static list gains a duplicate each time.
for (int i = 0; i < 100; i++) {
new SpringDocKotlinConfiguration();
}
System.out.println("DEPRECATED_ANNOTATIONS after 100 context starts: " + deprecatedAnnotations().size());
// 2. One context starting (writer) while another serves /v3/api-docs (reader, which calls isDeprecated
// for every handler method). isDeprecated streams the synchronizedList without holding its lock.
Method method = Repro.class.getMethod("endpoint");
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<?> writer = pool.submit(() -> {
for (int i = 0; i < 20_000; i++) {
new SpringDocKotlinConfiguration();
}
});
Future<?> reader = pool.submit(() -> {
while (!writer.isDone()) {
SchemaPropertyDeprecatingConverter.isDeprecated(method);
}
});
try {
reader.get();
System.out.println("No exception this run");
}
finally {
pool.shutdownNow();
}
}
private static Collection<?> deprecatedAnnotations() throws ReflectiveOperationException {
Field field = SchemaPropertyDeprecatingConverter.class.getDeclaredField("DEPRECATED_ANNOTATIONS");
field.setAccessible(true);
return (Collection<?>) field.get(null);
}
}
gradle run on 3.2.0 (JDK 26), 5 out of 5 runs:
DEPRECATED_ANNOTATIONS after 100 context starts: 101
Exception in thread "main" java.util.concurrent.ExecutionException: java.util.ConcurrentModificationException
...
Caused by: java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1703)
...
at org.springdoc.core.converters.SchemaPropertyDeprecatingConverter.isDeprecated(SchemaPropertyDeprecatingConverter.java:87)
new SpringDocKotlinConfiguration() stands in for a context start: it runs the same init block. The reflection only prints the size.
Expected behavior
isDeprecateddoes not throw while another context registers deprecated types.- Registering a type that is already registered does not grow the list: 100 context starts leave two entries (
java.lang.Deprecatedandkotlin.Deprecated).
Suggested fix
Make DEPRECATED_ANNOTATIONS a CopyOnWriteArraySet, like PolymorphicModelConverter.PARENT_TYPES_TO_IGNORE. That fixes both: iteration works on a snapshot, and add ignores a duplicate. The list is written a few times at startup and read on every docs generation, which suits copy-on-write. Every use (add, contains, stream) is also on Set, so nothing else changes. With that change the reproduction prints 2 and passes 5 out of 5 runs.
- Langage dominant
- Java
- Étoiles
- 3.7k
- Forks
- 605
- Merge moyen
- 4 j 18 h
- PR mergées (30 j)
- 21
Préparer son environnement
Ce projet ne fournit ni conteneur de développement, ni Dockerfile, ni guide de contribution : l'installation est à votre charge. Commencez par son README, et consultez notre guide de la première contribution pour les étapes générales.
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de springdoc/springdoc-openapi
-
Add support for OpenAPI spec 3.2Ouverteenhancement upstream
Difficulté 4/5 3-5 jours Accessibilité débutants 48/100
springdoc/springdoc-openapi#3324 · 2 commentaires ·
Les mainteneurs répondent en général sous 1 jour
-
enhancement upstream
Difficulté 3/5 1-2 jours Accessibilité débutants 55/100
springdoc/springdoc-openapi#3309 · 4 commentaires · 1 réaction ·
Les mainteneurs répondent en général sous 1 jour
Toutes les issues de springdoc/springdoc-openapi
Issues similaires
-
ARM: Q flag should be stickyOuverte
Difficulté 2/5 1-3 heures Accessibilité débutants 73/100
NationalSecurityAgency/ghidra#10155 ·
Les mainteneurs répondent en général sous 2 jours
-
bug web-ui
Difficulté 2/5 1-3 heures Accessibilité débutants 67/100
jenkinsci/jenkins#27705 · 3 commentaires ·
Les mainteneurs répondent en général sous 1 jour
-
"item-tooltip-keyboard-fix" crashes the game when opening the Items tab of the statistics menuOuvertemod bug
Difficulté 2/5 1-3 heures Accessibilité débutants 64/100
elmital/FixMCStats#65 · 3 commentaires ·
-
Difficulté 2/5 1-3 heures Accessibilité débutants 74/100
QuranHub/quranhub-android#354 ·
Les mainteneurs répondent en général sous 2 jours
-
Difficulté 2/5 1-3 heures Accessibilité débutants 80/100
googleads/googleads-mobile-flutter#1541 ·
Les mainteneurs répondent en général sous 4 jours