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:
@@ -20,6 +20,8 @@ import java.util.Collections;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.reactivestreams.Subscription;
|
||||
import reactor.core.publisher.BaseSubscriber;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
@@ -28,7 +30,6 @@ import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.LeakAwareDataBufferFactory;
|
||||
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
|
||||
@@ -36,19 +37,18 @@ import org.springframework.core.io.support.ResourceRegion;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.nio.charset.StandardCharsets.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Test cases for {@link ResourceRegionEncoder} class.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class ResourceRegionEncoderTests {
|
||||
|
||||
private ResourceRegionEncoder encoder = new ResourceRegionEncoder();
|
||||
|
||||
private DataBufferFactory bufferFactory = new LeakAwareDataBufferFactory();
|
||||
private LeakAwareDataBufferFactory bufferFactory = new LeakAwareDataBufferFactory();
|
||||
|
||||
|
||||
@Test
|
||||
@@ -79,10 +79,13 @@ public class ResourceRegionEncoderTests {
|
||||
.consumeNextWith(stringConsumer("Spring"))
|
||||
.expectComplete()
|
||||
.verify();
|
||||
|
||||
// TODO: https://github.com/reactor/reactor-core/issues/1634
|
||||
// this.bufferFactory.checkForLeaks();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldEncodeMultipleResourceRegionsFileResource() throws Exception {
|
||||
public void shouldEncodeMultipleResourceRegionsFileResource() {
|
||||
Resource resource = new ClassPathResource("ResourceRegionEncoderTests.txt", getClass());
|
||||
Flux<ResourceRegion> regions = Flux.just(
|
||||
new ResourceRegion(resource, 0, 6),
|
||||
@@ -118,6 +121,33 @@ public class ResourceRegionEncoderTests {
|
||||
.consumeNextWith(stringConsumer("\r\n--" + boundary + "--"))
|
||||
.expectComplete()
|
||||
.verify();
|
||||
|
||||
// TODO: https://github.com/reactor/reactor-core/issues/1634
|
||||
// this.bufferFactory.checkForLeaks();
|
||||
}
|
||||
|
||||
@Test // gh-
|
||||
public void cancelWithoutDemandForMultipleResourceRegions() {
|
||||
Resource resource = new ClassPathResource("ResourceRegionEncoderTests.txt", getClass());
|
||||
Flux<ResourceRegion> regions = Flux.just(
|
||||
new ResourceRegion(resource, 0, 6),
|
||||
new ResourceRegion(resource, 7, 9),
|
||||
new ResourceRegion(resource, 17, 4),
|
||||
new ResourceRegion(resource, 22, 17)
|
||||
);
|
||||
String boundary = MimeTypeUtils.generateMultipartBoundaryString();
|
||||
|
||||
Flux<DataBuffer> flux = this.encoder.encode(regions, this.bufferFactory,
|
||||
ResolvableType.forClass(ResourceRegion.class),
|
||||
MimeType.valueOf("text/plain"),
|
||||
Collections.singletonMap(ResourceRegionEncoder.BOUNDARY_STRING_HINT, boundary)
|
||||
);
|
||||
|
||||
ZeroDemandSubscriber subscriber = new ZeroDemandSubscriber();
|
||||
flux.subscribe(subscriber);
|
||||
subscriber.cancel();
|
||||
|
||||
this.bufferFactory.checkForLeaks();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -142,6 +172,9 @@ public class ResourceRegionEncoderTests {
|
||||
.consumeNextWith(stringConsumer("Spring"))
|
||||
.expectError(EncodingException.class)
|
||||
.verify();
|
||||
|
||||
// TODO: https://github.com/reactor/reactor-core/issues/1634
|
||||
// this.bufferFactory.checkForLeaks();
|
||||
}
|
||||
|
||||
protected Consumer<DataBuffer> stringConsumer(String expected) {
|
||||
@@ -154,4 +187,12 @@ public class ResourceRegionEncoderTests {
|
||||
}
|
||||
|
||||
|
||||
private static class ZeroDemandSubscriber extends BaseSubscriber<DataBuffer> {
|
||||
|
||||
@Override
|
||||
protected void hookOnSubscribe(Subscription subscription) {
|
||||
// Just subscribe without requesting
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -121,9 +121,23 @@ public abstract class AbstractDataBufferAllocatingTestCase {
|
||||
if (this.bufferFactory instanceof NettyDataBufferFactory) {
|
||||
ByteBufAllocator allocator = ((NettyDataBufferFactory) this.bufferFactory).getByteBufAllocator();
|
||||
if (allocator instanceof PooledByteBufAllocator) {
|
||||
PooledByteBufAllocatorMetric metric = ((PooledByteBufAllocator) allocator).metric();
|
||||
long total = getAllocations(metric.directArenas()) + getAllocations(metric.heapArenas());
|
||||
assertEquals("ByteBuf Leak: " + total + " unreleased allocations", 0, total);
|
||||
Instant start = Instant.now();
|
||||
while (true) {
|
||||
PooledByteBufAllocatorMetric metric = ((PooledByteBufAllocator) allocator).metric();
|
||||
long total = getAllocations(metric.directArenas()) + getAllocations(metric.heapArenas());
|
||||
if (total == 0) {
|
||||
return;
|
||||
}
|
||||
if (Instant.now().isBefore(start.plus(Duration.ofSeconds(5)))) {
|
||||
try {
|
||||
Thread.sleep(50);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
assertEquals("ByteBuf Leak: " + total + " unreleased allocations", 0, total);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -35,8 +35,11 @@ import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.reactivestreams.Subscription;
|
||||
import reactor.core.publisher.BaseSubscriber;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
@@ -184,6 +187,20 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase {
|
||||
.verify();
|
||||
}
|
||||
|
||||
// TODO: Remove ignore after https://github.com/reactor/reactor-core/issues/1634
|
||||
@Ignore
|
||||
@Test // gh-22107
|
||||
public void readAsynchronousFileChannelCancelWithoutDemand() throws Exception {
|
||||
URI uri = this.resource.getURI();
|
||||
Flux<DataBuffer> flux = DataBufferUtils.readAsynchronousFileChannel(
|
||||
() -> AsynchronousFileChannel.open(Paths.get(uri), StandardOpenOption.READ),
|
||||
this.bufferFactory, 3);
|
||||
|
||||
BaseSubscriber<DataBuffer> subscriber = new ZeroDemandSubscriber();
|
||||
flux.subscribe(subscriber);
|
||||
subscriber.cancel();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readResource() throws Exception {
|
||||
Flux<DataBuffer> flux = DataBufferUtils.read(this.resource, this.bufferFactory, 3);
|
||||
@@ -735,5 +752,12 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase {
|
||||
}
|
||||
|
||||
|
||||
private static class ZeroDemandSubscriber extends BaseSubscriber<DataBuffer> {
|
||||
|
||||
@Override
|
||||
protected void hookOnSubscribe(Subscription subscription) {
|
||||
// Just subscribe without requesting
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -17,9 +17,14 @@
|
||||
package org.springframework.core.io.buffer;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.After;
|
||||
|
||||
@@ -37,6 +42,9 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class LeakAwareDataBufferFactory implements DataBufferFactory {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(LeakAwareDataBufferFactory.class);
|
||||
|
||||
|
||||
private final DataBufferFactory delegate;
|
||||
|
||||
private final List<LeakAwareDataBuffer> created = new ArrayList<>();
|
||||
@@ -65,13 +73,27 @@ public class LeakAwareDataBufferFactory implements DataBufferFactory {
|
||||
* method.
|
||||
*/
|
||||
public void checkForLeaks() {
|
||||
this.created.stream()
|
||||
.filter(LeakAwareDataBuffer::isAllocated)
|
||||
.findFirst()
|
||||
.map(LeakAwareDataBuffer::leakError)
|
||||
.ifPresent(leakError -> {
|
||||
throw leakError;
|
||||
});
|
||||
Instant start = Instant.now();
|
||||
while (true) {
|
||||
if (this.created.stream().noneMatch(LeakAwareDataBuffer::isAllocated)) {
|
||||
return;
|
||||
}
|
||||
if (Instant.now().isBefore(start.plus(Duration.ofSeconds(5)))) {
|
||||
try {
|
||||
Thread.sleep(50);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
List<AssertionError> errors = this.created.stream()
|
||||
.filter(LeakAwareDataBuffer::isAllocated)
|
||||
.map(LeakAwareDataBuffer::leakError)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
errors.forEach(it -> logger.error("Leaked error: ", it));
|
||||
throw new AssertionError(errors.size() + " buffer leaks detected (see logs above)");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user