Skip to content

Commit beae1fb

Browse files
committed
Uncomment buffer leak tests in DataBufferUtils
Following a response on reactor/reactor-core#1634 apparently FluxSink is respecting doOnDiscard but there is a race condition in DataBufferUtils with file reading. This commit makes two changes: 1) Add more checks for onDispose to detect cancellation signals as well as to deal with potentially concurrent such signal. 2) Do not close the channel through the Flux.using callback but rather allow the current I/O callback to take place and only then close the channel or else the buffer is left hanging. Despite this tests still can fail due to a suspected issue in Reactor itself with the doOnDiscard callback for FluxSink. That's tracked under the same issue reactor/reactor-core#1634 and for now the use of DefaultDataBufferFactory is enforced as a workaround until the issue is resolved. See gh-22107
1 parent 3cf2c04 commit beae1fb

File tree

5 files changed

+38
-16
lines changed

5 files changed

+38
-16
lines changed

spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ public abstract class DataBufferUtils {
5757

5858
private static final Consumer<DataBuffer> RELEASE_CONSUMER = DataBufferUtils::release;
5959

60+
/**
61+
* Workaround to disable use of pooled buffers:
62+
* https://github.com/reactor/reactor-core/issues/1634
63+
*/
64+
private static final DataBufferFactory defaultDataBufferFactory = new DefaultDataBufferFactory();
65+
6066

6167
//---------------------------------------------------------------------
6268
// Reading
@@ -132,16 +138,21 @@ public static Flux<DataBuffer> readAsynchronousFileChannel(Callable<Asynchronous
132138
Assert.isTrue(position >= 0, "'position' must be >= 0");
133139
Assert.isTrue(bufferSize > 0, "'bufferSize' must be > 0");
134140

141+
DataBufferFactory bufferFactoryToUse = defaultDataBufferFactory;
142+
135143
Flux<DataBuffer> flux = Flux.using(channelSupplier,
136144
channel -> Flux.create(sink -> {
137145
ReadCompletionHandler handler =
138-
new ReadCompletionHandler(channel, sink, position, bufferFactory, bufferSize);
139-
DataBuffer dataBuffer = bufferFactory.allocateBuffer(bufferSize);
146+
new ReadCompletionHandler(channel, sink, position, bufferFactoryToUse, bufferSize);
147+
DataBuffer dataBuffer = bufferFactoryToUse.allocateBuffer(bufferSize);
140148
ByteBuffer byteBuffer = dataBuffer.asByteBuffer(0, bufferSize);
141149
channel.read(byteBuffer, position, dataBuffer, handler);
142150
sink.onDispose(handler::dispose);
143151
}),
144-
DataBufferUtils::closeChannel);
152+
channel -> {
153+
// Do not close channel from here, rather wait for the current read callback
154+
// and then complete after releasing the DataBuffer.
155+
});
145156

146157
return flux.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
147158
}
@@ -505,26 +516,40 @@ public ReadCompletionHandler(AsynchronousFileChannel channel,
505516

506517
@Override
507518
public void completed(Integer read, DataBuffer dataBuffer) {
508-
if (read != -1) {
519+
if (read != -1 && !this.disposed.get()) {
509520
long pos = this.position.addAndGet(read);
510521
dataBuffer.writePosition(read);
511522
this.sink.next(dataBuffer);
512-
if (!this.disposed.get()) {
523+
// It's possible for cancellation to happen right before the push into the sink
524+
if (this.disposed.get()) {
525+
// TODO:
526+
// This is not ideal since we already passed the buffer into the sink and
527+
// releasing may cause something reading to fail. Maybe we won't have to
528+
// do this after https://github.com/reactor/reactor-core/issues/1634
529+
complete(dataBuffer);
530+
}
531+
else {
513532
DataBuffer newDataBuffer = this.dataBufferFactory.allocateBuffer(this.bufferSize);
514533
ByteBuffer newByteBuffer = newDataBuffer.asByteBuffer(0, this.bufferSize);
515534
this.channel.read(newByteBuffer, pos, newDataBuffer, this);
516535
}
517536
}
518537
else {
519-
release(dataBuffer);
520-
this.sink.complete();
538+
complete(dataBuffer);
521539
}
522540
}
523541

542+
private void complete(DataBuffer dataBuffer) {
543+
release(dataBuffer);
544+
this.sink.complete();
545+
closeChannel(this.channel);
546+
}
547+
524548
@Override
525549
public void failed(Throwable exc, DataBuffer dataBuffer) {
526550
release(dataBuffer);
527551
this.sink.error(exc);
552+
closeChannel(this.channel);
528553
}
529554

530555
public void dispose() {

spring-core/src/test/java/org/springframework/core/codec/ResourceRegionEncoderTests.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ public void shouldEncodeResourceRegionFileResource() throws Exception {
8080
.expectComplete()
8181
.verify();
8282

83-
// TODO: https://github.com/reactor/reactor-core/issues/1634
84-
// this.bufferFactory.checkForLeaks();
83+
this.bufferFactory.checkForLeaks();
8584
}
8685

8786
@Test
@@ -122,11 +121,10 @@ public void shouldEncodeMultipleResourceRegionsFileResource() {
122121
.expectComplete()
123122
.verify();
124123

125-
// TODO: https://github.com/reactor/reactor-core/issues/1634
126-
// this.bufferFactory.checkForLeaks();
124+
this.bufferFactory.checkForLeaks();
127125
}
128126

129-
@Test // gh-
127+
@Test // gh-22107
130128
public void cancelWithoutDemandForMultipleResourceRegions() {
131129
Resource resource = new ClassPathResource("ResourceRegionEncoderTests.txt", getClass());
132130
Flux<ResourceRegion> regions = Flux.just(
@@ -173,8 +171,7 @@ public void nonExisting() {
173171
.expectError(EncodingException.class)
174172
.verify();
175173

176-
// TODO: https://github.com/reactor/reactor-core/issues/1634
177-
// this.bufferFactory.checkForLeaks();
174+
this.bufferFactory.checkForLeaks();
178175
}
179176

180177
protected Consumer<DataBuffer> stringConsumer(String expected) {

spring-core/src/test/java/org/springframework/core/io/buffer/AbstractDataBufferAllocatingTestCase.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ private void verifyAllocations() {
135135
catch (InterruptedException ex) {
136136
// ignore
137137
}
138+
continue;
138139
}
139140
assertEquals("ByteBuf Leak: " + total + " unreleased allocations", 0, total);
140141
}

spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,6 @@ public void readAsynchronousFileChannelCancel() throws Exception {
187187
.verify();
188188
}
189189

190-
// TODO: Remove ignore after https://github.com/reactor/reactor-core/issues/1634
191-
@Ignore
192190
@Test // gh-22107
193191
public void readAsynchronousFileChannelCancelWithoutDemand() throws Exception {
194192
URI uri = this.resource.getURI();

spring-core/src/test/java/org/springframework/core/io/buffer/LeakAwareDataBufferFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public void checkForLeaks() {
8585
catch (InterruptedException ex) {
8686
// ignore
8787
}
88+
continue;
8889
}
8990
List<AssertionError> errors = this.created.stream()
9091
.filter(LeakAwareDataBuffer::isAllocated)

0 commit comments

Comments
 (0)