reactor / reactor-core

Non-Blocking Reactive Foundation for the JVM

Home Page:http://projectreactor.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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