Refactor maxInMemory limit handling for async XML parsing

The limit was previously enforced in XmlEventDecoder, because it is
what parses incoming buffers. However, the actual caching is in
Jaxb2Decoder, which holds on to XML events, but has no good way to
estimate their size.

After this commit XmlEventDecoder no longer enforces memory limits
for async parsing. It releases each buffer immediately anyway.

Instead XmlEventDecoder is only responsible to update the number
of bytes received via a new ReceivedByteTracker type while
Jaxb2XmlDecoder uses the same to perform limit and reset the
count depending on when it is aggregating XML events.

Closes gh-37031
This commit is contained in:
rstoyanchev
2026-08-14 09:11:50 +02:00
committed by Brian Clozel
parent 675f25de72
commit 73f5ddddcd
5 changed files with 130 additions and 84 deletions
@@ -35,7 +35,9 @@ import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.codec.Hints;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
import org.springframework.http.MediaType;
import org.springframework.util.MimeType;
@@ -92,7 +94,7 @@ class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
@Test
void splitOneBranches() {
Flux<XMLEvent> xmlEvents = this.xmlEventDecoder.decode(toDataBufferMono(POJO_ROOT), null, null, HINTS);
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")));
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")), null);
StepVerifier.create(result)
.consumeNextWith(events -> {
@@ -113,8 +115,7 @@ class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
@Test
void splitMultipleBranches() {
Flux<XMLEvent> xmlEvents = this.xmlEventDecoder.decode(toDataBufferMono(POJO_CHILD), null, null, HINTS);
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")));
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")), null);
StepVerifier.create(result)
.consumeNextWith(events -> {
@@ -143,6 +144,34 @@ class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
.verify();
}
@Test
void splitMultipleBranchesLimitExceeded() {
Flux<String> source = Flux.just(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
"<root><pojo><foo>foo</foo></pojo>",
"<pojo><foo>foofoo</foo>",
"<bar>barbar</bar></pojo>",
"<root/>");
XmlEventDecoder.ReceivedByteTracker byteTracker = new XmlEventDecoder.ReceivedByteTracker(30);
Map<String, Object> hints = Hints.from(XmlEventDecoder.BYTE_TRACKER_HINT, byteTracker);
Flux<XMLEvent> xmlEvents = this.xmlEventDecoder.decode(source.map(this::toToDataBuffer), null, null, hints);
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")), byteTracker);
StepVerifier.create(result)
.consumeNextWith(events -> {
assertThat(events).hasSize(5);
assertStartElement(events.get(0), "pojo");
assertStartElement(events.get(1), "foo");
assertCharacters(events.get(2), "foo");
assertEndElement(events.get(3), "foo");
assertEndElement(events.get(4), "pojo");
})
.expectError(DataBufferLimitException.class)
.verify();
}
private static void assertStartElement(XMLEvent event, String expectedLocalName) {
assertThat(event.isStartElement()).isTrue();
assertThat(event.asStartElement().getName().getLocalPart()).isEqualTo(expectedLocalName);
@@ -263,13 +292,18 @@ class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
private Mono<DataBuffer> toDataBufferMono(String value) {
return Mono.defer(() -> {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
DataBuffer buffer = toToDataBuffer(value);
return Mono.just(buffer);
});
}
private DataBuffer toToDataBuffer(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
@jakarta.xml.bind.annotation.XmlType
@XmlSeeAlso(Child.class)
public abstract static class Parent {
@@ -27,7 +27,6 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
import static org.assertj.core.api.Assertions.assertThat;
@@ -88,28 +87,6 @@ class XmlEventDecoderTests extends AbstractLeakCheckingTests {
.verify();
}
@Test
void toXMLEventsWithLimit() {
this.decoder.setMaxInMemorySize(6);
Flux<String> source = Flux.just(
"<pojo>", "<foo>", "foofoo", "</foo>", "<bar>", "barbarbar", "</bar>", "</pojo>");
Flux<XMLEvent> events = this.decoder.decode(
source.map(this::stringBuffer), null, null, Collections.emptyMap());
StepVerifier.create(events)
.consumeNextWith(e -> assertThat(e.isStartDocument()).isTrue())
.consumeNextWith(e -> assertStartElement(e, "pojo"))
.consumeNextWith(e -> assertStartElement(e, "foo"))
.consumeNextWith(e -> assertCharacters(e, "foofoo"))
.consumeNextWith(e -> assertEndElement(e, "foo"))
.consumeNextWith(e -> assertStartElement(e, "bar"))
.expectError(DataBufferLimitException.class)
.verify();
}
@Test
void decodeErrorAalto() {
Flux<DataBuffer> source = Flux.concat(