Buffer leak fixes
Address issues where buffers are allocated (and cached somehow) at or before subscription, and before explicit demand. The commit adds tests proving the leaks and fixes. The common thread for all tests is a "zero demand" subscriber that subscribes but does not request, and then cancels without consuming anything. Closes gh-22107
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.http.codec;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.reactivestreams.Subscription;
|
||||
import reactor.core.publisher.BaseSubscriber;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.LeakAwareDataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ReactiveHttpOutputMessage;
|
||||
import org.springframework.http.client.MultipartBodyBuilder;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
|
||||
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
||||
|
||||
/**
|
||||
* Test scenarios for data buffer leaks.
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.2
|
||||
*/
|
||||
public class CodecDataBufferLeakTests {
|
||||
|
||||
private final LeakAwareDataBufferFactory bufferFactory = new LeakAwareDataBufferFactory();
|
||||
|
||||
|
||||
@Test // gh-22107
|
||||
public void cancelWithEncoderHttpMessageWriterAndSingleValue() {
|
||||
CharSequenceEncoder encoder = CharSequenceEncoder.allMimeTypes();
|
||||
HttpMessageWriter<CharSequence> writer = new EncoderHttpMessageWriter<>(encoder);
|
||||
CancellingOutputMessage outputMessage = new CancellingOutputMessage(this.bufferFactory);
|
||||
|
||||
writer.write(Mono.just("foo"), ResolvableType.forType(String.class), MediaType.TEXT_PLAIN,
|
||||
outputMessage, Collections.emptyMap()).block(Duration.ofSeconds(5));
|
||||
|
||||
this.bufferFactory.checkForLeaks();
|
||||
}
|
||||
|
||||
@Test // gh-22107
|
||||
public void cancelWithJackson() {
|
||||
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
|
||||
|
||||
Flux<DataBuffer> flux = encoder.encode(Flux.just(new Pojo("foofoo", "barbar"), new Pojo("bar", "baz")),
|
||||
this.bufferFactory, ResolvableType.forClass(Pojo.class),
|
||||
MediaType.APPLICATION_JSON, Collections.emptyMap());
|
||||
|
||||
BaseSubscriber<DataBuffer> subscriber = new ZeroDemandSubscriber();
|
||||
flux.subscribe(subscriber); // Assume sync execution (e.g. encoding with Flux.just)..
|
||||
subscriber.cancel();
|
||||
|
||||
this.bufferFactory.checkForLeaks();
|
||||
}
|
||||
|
||||
@Test // gh-22107
|
||||
public void cancelWithJaxb2() {
|
||||
Jaxb2XmlEncoder encoder = new Jaxb2XmlEncoder();
|
||||
|
||||
Flux<DataBuffer> flux = encoder.encode(Mono.just(new Pojo("foo", "bar")),
|
||||
this.bufferFactory, ResolvableType.forClass(Pojo.class),
|
||||
MediaType.APPLICATION_XML, Collections.emptyMap());
|
||||
|
||||
BaseSubscriber<DataBuffer> subscriber = new ZeroDemandSubscriber();
|
||||
flux.subscribe(subscriber); // Assume sync execution (e.g. encoding with Flux.just)..
|
||||
subscriber.cancel();
|
||||
|
||||
this.bufferFactory.checkForLeaks();
|
||||
}
|
||||
|
||||
@Test // gh-22107
|
||||
public void cancelWithMultipartContent() {
|
||||
MultipartBodyBuilder builder = new MultipartBodyBuilder();
|
||||
builder.part("part1", "value1");
|
||||
builder.part("part2", "value2");
|
||||
|
||||
List<HttpMessageWriter<?>> writers = ClientCodecConfigurer.create().getWriters();
|
||||
MultipartHttpMessageWriter writer = new MultipartHttpMessageWriter(writers);
|
||||
CancellingOutputMessage outputMessage = new CancellingOutputMessage(this.bufferFactory);
|
||||
|
||||
writer.write(Mono.just(builder.build()), null, MediaType.MULTIPART_FORM_DATA,
|
||||
outputMessage, Collections.emptyMap()).block(Duration.ofSeconds(5));
|
||||
|
||||
this.bufferFactory.checkForLeaks();
|
||||
}
|
||||
|
||||
@Test // gh-22107
|
||||
public void cancelWithSse() {
|
||||
ServerSentEvent<?> event = ServerSentEvent.builder().data("bar").id("c42").event("foo").build();
|
||||
ServerSentEventHttpMessageWriter writer = new ServerSentEventHttpMessageWriter(new Jackson2JsonEncoder());
|
||||
CancellingOutputMessage outputMessage = new CancellingOutputMessage(this.bufferFactory);
|
||||
|
||||
writer.write(Mono.just(event), ResolvableType.forClass(ServerSentEvent.class), MediaType.TEXT_EVENT_STREAM,
|
||||
outputMessage, Collections.emptyMap()).block(Duration.ofSeconds(5));
|
||||
|
||||
this.bufferFactory.checkForLeaks();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static class CancellingOutputMessage implements ReactiveHttpOutputMessage {
|
||||
|
||||
private final DataBufferFactory bufferFactory;
|
||||
|
||||
|
||||
public CancellingOutputMessage(DataBufferFactory bufferFactory) {
|
||||
this.bufferFactory = bufferFactory;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public DataBufferFactory bufferFactory() {
|
||||
return this.bufferFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeCommit(Supplier<? extends Mono<Void>> action) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCommitted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
|
||||
Flux<? extends DataBuffer> flux = Flux.from(body);
|
||||
BaseSubscriber<DataBuffer> subscriber = new ZeroDemandSubscriber();
|
||||
flux.subscribe(subscriber); // Assume sync execution (e.g. encoding with Flux.just)..
|
||||
subscriber.cancel();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
|
||||
Flux<? extends DataBuffer> flux = Flux.from(body).concatMap(Flux::from);
|
||||
BaseSubscriber<DataBuffer> subscriber = new ZeroDemandSubscriber();
|
||||
flux.subscribe(subscriber); // Assume sync execution (e.g. encoding with Flux.just)..
|
||||
subscriber.cancel();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> setComplete() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return new HttpHeaders();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class ZeroDemandSubscriber extends BaseSubscriber<DataBuffer> {
|
||||
|
||||
@Override
|
||||
protected void hookOnSubscribe(Subscription subscription) {
|
||||
// Just subscribe without requesting
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,8 +197,11 @@ public class MultipartHttpMessageWriterTests extends AbstractLeakCheckingTestCas
|
||||
|
||||
Mono<MultiValueMap<String, HttpEntity<?>>> result = Mono.just(bodyBuilder.build());
|
||||
|
||||
Map<String, Object> hints = Collections.emptyMap();
|
||||
this.writer.write(result, null, MediaType.MULTIPART_FORM_DATA, this.response, hints).block();
|
||||
this.writer.write(result, null, MediaType.MULTIPART_FORM_DATA, this.response, Collections.emptyMap())
|
||||
.block(Duration.ofSeconds(5));
|
||||
|
||||
// Make sure body is consumed to avoid leak reports
|
||||
this.response.getBodyAsString().block(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@Test // SPR-16376
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -147,6 +147,7 @@ public class MockServerHttpResponse extends AbstractServerHttpResponse {
|
||||
Assert.notNull(charset, "'charset' must not be null");
|
||||
byte[] bytes = new byte[buffer.readableByteCount()];
|
||||
buffer.read(bytes);
|
||||
DataBufferUtils.release(buffer);
|
||||
return new String(bytes, charset);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user