`ConcurrentModificationException` in `SchemaPropertyDeprecatingConverter.isDeprecated`, and `DEPRECATED_ANNOTATIONS` gains a duplicate on every context start
Los mantenedores suelen responder en 9 días
Evaluación
- Dificultad
- 2/5
- Tiempo estimado
- 1-3 horas
- Aptitud para principiantes
- 85/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Área
- api, backend-api-design
Línea de trabajo
Empieza con SchemaPropertyDeprecatingConverter.isDeprecated y la declaración de DEPRECATED_ANNOTATIONS en springdoc-openapi-starter-common; compara el comportamiento de sus colecciones con el de las colecciones de copia en escritura mencionadas en el issue. Usa la reproducción descrita en el issue para comprobar el registro concurrente y el registro repetido. El trabajo está terminado cuando las llamadas concurrentes no lanzan excepciones y el registro repetido deja solo los dos tipos de anotación esperados.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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.
- Lenguaje dominante
- Java
- Estrellas
- 3.7k
- Forks
- 605
- Merge medio
- 9 d 11 h
- PR fusionados (30 d)
- 14
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de springdoc/springdoc-openapi
-
Add support for OpenAPI spec 3.2Abiertoenhancement upstream
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
springdoc/springdoc-openapi#3324 · 2 comentarios ·
Los mantenedores suelen responder en 9 días
-
enhancement upstream
Dificultad 3/5 1-2 días Aptitud para principiantes 55/100
springdoc/springdoc-openapi#3309 · 4 comentarios · 1 reacción ·
Los mantenedores suelen responder en 9 días
-
Dificultad 3/5 1-2 días Aptitud para principiantes 45/100
springdoc/springdoc-openapi#3404 · 1 comentario ·
Los mantenedores suelen responder en 9 días
Todos los issues de springdoc/springdoc-openapi
Issues similares
-
Good First Issue
Dificultad 1/5 Menos de una hora Aptitud para principiantes 68/100
hiero-ledger/hiero-sdk-java#3044 ·
Los mantenedores suelen responder en 1 día
-
A second build without clean fails in resources-optimizer-maven-plugin because minified files are minified againPosiblemente ocupada @papegaaij la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
hermes-queue
Dificultad 1/5 Menos de una hora Aptitud para principiantes 85/100
chrisalunlloyd2-sudo/mindpalace#337 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
Los mantenedores suelen responder en 1 día
-
[Regression][98.0.0] Android I/flutter logs are automatically folded by FlutterConsoleFoldingAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 79/100
flutter/flutter-intellij#9221 ·
Los mantenedores suelen responder en 1 día