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:
Rossen Stoyanchev
2019-03-26 21:11:19 -04:00
parent 65b46079a2
commit c54355784e
16 changed files with 504 additions and 211 deletions

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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);
}