Flux/Mono.usingWhen late resource memory leak
mlozbin opened this issue · comments
Depending on the resource acquisition mechanism usingWhen method may lead to a memory or resource leak. Latest related fixes assured that provided resource will be closed or subscription will be cancelled. The problem is that cancellation process most often is not deterministic and either not intuitive or impossible to implement (side effects during resource creation). Yes, sometimes cancelling the resource subscription may save some resources if we cancel it but in most cases it leads to resource leaks that are very hard to find and fix (the amount of existing issues for this method is a good proof of this point).
This issue is a result of investigation of previously created issues (#1233, #2661, #124, #2836) and should meet all the requirements.
Expected Behavior
asyncCancel (respective asyncCleanup) should be always called.
Actual Behavior
Resource creation process was started but pipeline was cancelled so cleanup never happened.
Steps to Reproduce
Ensure some delay in the resource allocation
Cancel the main pipeline before the resource is emitted.
Below is the modified version of an existing test that shows that problem still exists if resource allocation is long. Existing test passes because resource allocation is delayed (so can be cancelled) but not long.
Random is not needed since problem is 100% reproducible.
@Test
void cancelEarlyDoesNotLeak() {
Releaseable releaseable = new Releaseable();
Mono<Long> mono = Mono.usingWhen(Mono.fromSupplier(() -> {
LockSupport.parkNanos(Duration.ofMillis(150).toNanos());
releaseable.allocate();
return releaseable;
}), it -> Mono.just(1L), it -> Mono.fromRunnable(releaseable::release));
StepVerifier.create(mono, 1)
.thenAwait(Duration.ofMillis(50))
.thenCancel()
.verify();
Awaitility.await()
.atMost(Duration.ofSeconds(10))
.until(releaseable::wasAcquired);
Awaitility.await()
.atMost(Duration.ofSeconds(10))
.until(releaseable::wasReleased);
}
static class Releaseable {
private final AtomicBoolean allocated = new AtomicBoolean();
private final AtomicBoolean released = new AtomicBoolean();
void allocate() {
System.out.println("alloc");
allocated.set(true);
}
void release() {
System.out.println("release");
released.set(true);
}
boolean wasReleased() {
return released.get();
}
boolean wasAcquired() {
return released.get();
}
}
Possible Solution
Remove this cancel method override from ResourceSubscriber that forces early cancel for resource pipeline. That will allow the process to gracefully finish, emit the resource and do a cleanup.
@Override
public void cancel() {
if (!resourceProvided) {
resourceSubscription.cancel();
}
super.cancel();
}
Your Environment
Reactor version(s) used: 3.6.3
Other relevant libraries versions (eg. netty, ...): -
JVM version (java -version): Java 17
OS and version (eg uname -a): Windows 11