`ConcurrentModificationException` in `SchemaPropertyDeprecatingConverter.isDeprecated`, and `DEPRECATED_ANNOTATIONS` gains a duplicate on every context start
Maintainer antworten meist innerhalb von 1 Tag
Bewertung
- Schwierigkeit
- 2/5
- Geschätzter Aufwand
- 1-3 Stunden
- Anfängerfreundlichkeit
- 85/100
- Issue-Typ
- Bug
- Klarheit
- Klar beschrieben
- Aktivitätsstatus
- Aktiv
- Bereich
- api, backend-api-design
Rechercherichtung
Beginne mit SchemaPropertyDeprecatingConverter.isDeprecated und der Deklaration von DEPRECATED_ANNOTATIONS in springdoc-openapi-starter-common; vergleiche ihr Verhalten bei Sammlungen mit den im Issue erwähnten Copy-on-Write-Sammlungen. Verwende die im Issue beschriebene Reproduktion, um die gleichzeitige Registrierung und wiederholte Registrierung zu prüfen. Fertig ist die Änderung, wenn gleichzeitige Aufrufe keine Exception auslösen und nach wiederholter Registrierung nur die beiden erwarteten Annotationstypen übrig bleiben.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
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.
- Vorherrschende Sprache
- Java
- Sterne
- 3.7k
- Forks
- 605
- Ø Merge
- 3 T. 15 Std.
- Gemergte PRs (30 T.)
- 19
Entwicklungsumgebung
Dieses Projekt bietet weder Dev-Container noch Dockerfile noch Beitragsleitfaden – die Einrichtung liegt bei Ihnen. Beginnen Sie mit der README; die allgemeinen Schritte stehen in unserem Leitfaden für den ersten Beitrag.
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus springdoc/springdoc-openapi
-
MCP: OpenApiSchemaConverter drops types from OpenAPI 3.1 type arraysEvtl. vergeben @mariodavid hat das heute übernommen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 30/100
springdoc/springdoc-openapi#3427 ·
Maintainer antworten meist innerhalb von 1 Tag
-
enhancement upstream
Schwierigkeit 4/5 3-5 Tage Anfängerfreundlichkeit 48/100
springdoc/springdoc-openapi#3324 · 2 Kommentare ·
Maintainer antworten meist innerhalb von 1 Tag
-
enhancement upstream
Schwierigkeit 3/5 1-2 Tage Anfängerfreundlichkeit 55/100
springdoc/springdoc-openapi#3309 · 4 Kommentare · 1 Reaktion ·
Maintainer antworten meist innerhalb von 1 Tag
Alle Issues in springdoc/springdoc-openapi
Ähnliche Issues
-
hermes-queue
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 72/100
chrisalunlloyd2-sudo/mindpalace#349 ·
Maintainer antworten meist innerhalb von 1 Tag
-
Retry-After parser accepts invalid delay-seconds valuesEvtl. vergeben @Dhruvraj-Rana hat das heute übernommen. OffenBug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 82/100
open-telemetry/opentelemetry-java#9018 ·
Maintainer antworten meist innerhalb von 2 Tagen
-
bug priority: medium Review effort [1-5]: 2 status: needs-triage type: bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
carlos-emr/carlos#5818 ·
Maintainer antworten meist innerhalb von 1 Tag
-
bug good first issue help wanted tooling
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
appmonster007/kmip-adapter#100 ·
-
accepted good first issue java
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 88/100
Maintainer antworten meist innerhalb von 1 Tag