Skip to content

Commit a5706bf

Browse files
authored
provides more ByteBuf leaks fixes (#803)
1. Ensures if there goes something during `Payload` to the frame we will not get any memory leaks in the end 2. Fixes `onDiscard` leak which did not work correctly because `actual` subscriber was nulled to early 3. Ensures that we will not have the wrong frame order in case of racing the first `Payload` sending and cancel. Also, it moves `onDiscard` hook to the very bottom in case of `requestChannel` to ensure the first payload is not leaked 4. Enables a set of tests that ensures that #757 and #733 are fully or partially fixed.
1 parent 1c3927b commit a5706bf

14 files changed

+437
-180
lines changed

rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java

Lines changed: 106 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
import io.rsocket.util.MonoLifecycleHandler;
5555
import java.nio.channels.ClosedChannelException;
5656
import java.util.concurrent.CancellationException;
57-
import java.util.concurrent.atomic.AtomicBoolean;
57+
import java.util.concurrent.atomic.AtomicInteger;
5858
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
5959
import java.util.function.Consumer;
6060
import java.util.function.LongConsumer;
@@ -260,6 +260,7 @@ public void doOnTerminal(
260260
removeStreamReceiver(streamId);
261261
}
262262
});
263+
263264
receivers.put(streamId, receiver);
264265

265266
return receiver.doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER);
@@ -281,7 +282,7 @@ private Flux<Payload> handleRequestStream(final Payload payload) {
281282

282283
final UnboundedProcessor<ByteBuf> sendProcessor = this.sendProcessor;
283284
final UnicastProcessor<Payload> receiver = UnicastProcessor.create();
284-
final AtomicBoolean payloadReleasedFlag = new AtomicBoolean(false);
285+
final AtomicInteger wip = new AtomicInteger(0);
285286

286287
receivers.put(streamId, receiver);
287288

@@ -293,30 +294,49 @@ private Flux<Payload> handleRequestStream(final Payload payload) {
293294

294295
@Override
295296
public void accept(long n) {
296-
if (firstRequest && !receiver.isDisposed()) {
297+
if (firstRequest) {
297298
firstRequest = false;
298-
if (!payloadReleasedFlag.getAndSet(true)) {
299-
sendProcessor.onNext(
300-
RequestStreamFrameFlyweight.encodeReleasingPayload(
301-
allocator, streamId, n, payload));
299+
if (wip.getAndIncrement() != 0) {
300+
// no need to do anything.
301+
// stream was canceled and fist payload has already been discarded
302+
return;
302303
}
303-
} else if (contains(streamId) && !receiver.isDisposed()) {
304+
int missed = 1;
305+
boolean firstHasBeenSent = false;
306+
for (; ; ) {
307+
if (!firstHasBeenSent) {
308+
sendProcessor.onNext(
309+
RequestStreamFrameFlyweight.encodeReleasingPayload(
310+
allocator, streamId, n, payload));
311+
firstHasBeenSent = true;
312+
} else {
313+
// if first frame was sent but we cycling again, it means that wip was
314+
// incremented at doOnCancel
315+
sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId));
316+
return;
317+
}
318+
319+
missed = wip.addAndGet(-missed);
320+
if (missed == 0) {
321+
return;
322+
}
323+
}
324+
} else if (!receiver.isDisposed()) {
304325
sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n));
305326
}
306327
}
307328
})
308-
.doOnError(
309-
t -> {
310-
if (contains(streamId) && !receiver.isDisposed()) {
311-
sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t));
312-
}
313-
})
314329
.doOnCancel(
315330
() -> {
316-
if (!payloadReleasedFlag.getAndSet(true)) {
317-
payload.release();
331+
if (wip.getAndIncrement() != 0) {
332+
return;
318333
}
319-
if (contains(streamId) && !receiver.isDisposed()) {
334+
335+
// check if we need to release payload
336+
// only applicable if the cancel appears earlier than actual request
337+
if (payload.refCnt() > 0) {
338+
payload.release();
339+
} else {
320340
sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId));
321341
}
322342
})
@@ -330,30 +350,32 @@ private Flux<Payload> handleChannel(Flux<Payload> request) {
330350
return Flux.error(err);
331351
}
332352

333-
return request.switchOnFirst(
334-
(s, flux) -> {
335-
Payload payload = s.get();
336-
if (payload != null) {
337-
if (!PayloadValidationUtils.isValid(mtu, payload)) {
338-
payload.release();
339-
final IllegalArgumentException t =
340-
new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE);
341-
errorConsumer.accept(t);
342-
return Mono.error(t);
343-
}
344-
return handleChannel(payload, flux);
345-
} else {
346-
return flux;
347-
}
348-
},
349-
false);
353+
return request
354+
.switchOnFirst(
355+
(s, flux) -> {
356+
Payload payload = s.get();
357+
if (payload != null) {
358+
if (!PayloadValidationUtils.isValid(mtu, payload)) {
359+
payload.release();
360+
final IllegalArgumentException t =
361+
new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE);
362+
errorConsumer.accept(t);
363+
return Mono.error(t);
364+
}
365+
return handleChannel(payload, flux);
366+
} else {
367+
return flux;
368+
}
369+
},
370+
false)
371+
.doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER);
350372
}
351373

352374
private Flux<? extends Payload> handleChannel(Payload initialPayload, Flux<Payload> inboundFlux) {
353375
final UnboundedProcessor<ByteBuf> sendProcessor = this.sendProcessor;
354-
final AtomicBoolean payloadReleasedFlag = new AtomicBoolean(false);
355376
final int streamId = streamIdSupplier.nextStreamId(receivers);
356377

378+
final AtomicInteger wip = new AtomicInteger(0);
357379
final UnicastProcessor<Payload> receiver = UnicastProcessor.create();
358380
final BaseSubscriber<Payload> upstreamSubscriber =
359381
new BaseSubscriber<Payload>() {
@@ -421,19 +443,47 @@ protected void hookFinally(SignalType type) {
421443
public void accept(long n) {
422444
if (firstRequest) {
423445
firstRequest = false;
424-
senders.put(streamId, upstreamSubscriber);
425-
receivers.put(streamId, receiver);
426-
427-
inboundFlux
428-
.limitRate(Queues.SMALL_BUFFER_SIZE)
429-
.doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER)
430-
.subscribe(upstreamSubscriber);
431-
if (!payloadReleasedFlag.getAndSet(true)) {
432-
ByteBuf frame =
433-
RequestChannelFrameFlyweight.encodeReleasingPayload(
434-
allocator, streamId, false, n, initialPayload);
435-
436-
sendProcessor.onNext(frame);
446+
if (wip.getAndIncrement() != 0) {
447+
// no need to do anything.
448+
// stream was canceled and fist payload has already been discarded
449+
return;
450+
}
451+
int missed = 1;
452+
boolean firstHasBeenSent = false;
453+
for (; ; ) {
454+
if (!firstHasBeenSent) {
455+
ByteBuf frame;
456+
try {
457+
frame =
458+
RequestChannelFrameFlyweight.encodeReleasingPayload(
459+
allocator, streamId, false, n, initialPayload);
460+
} catch (IllegalReferenceCountException e) {
461+
return;
462+
}
463+
464+
senders.put(streamId, upstreamSubscriber);
465+
receivers.put(streamId, receiver);
466+
467+
inboundFlux
468+
.limitRate(Queues.SMALL_BUFFER_SIZE)
469+
.doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER)
470+
.subscribe(upstreamSubscriber);
471+
472+
sendProcessor.onNext(frame);
473+
firstHasBeenSent = true;
474+
} else {
475+
// if first frame was sent but we cycling again, it means that wip was
476+
// incremented at doOnCancel
477+
senders.remove(streamId, upstreamSubscriber);
478+
receivers.remove(streamId, receiver);
479+
sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId));
480+
return;
481+
}
482+
483+
missed = wip.addAndGet(-missed);
484+
if (missed == 0) {
485+
return;
486+
}
437487
}
438488
} else {
439489
sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n));
@@ -442,22 +492,22 @@ public void accept(long n) {
442492
})
443493
.doOnError(
444494
t -> {
445-
if (receivers.remove(streamId, receiver)) {
446-
upstreamSubscriber.cancel();
447-
}
495+
upstreamSubscriber.cancel();
496+
receivers.remove(streamId, receiver);
448497
})
449498
.doOnComplete(() -> receivers.remove(streamId, receiver))
450499
.doOnCancel(
451500
() -> {
452-
if (!payloadReleasedFlag.getAndSet(true)) {
453-
initialPayload.release();
501+
upstreamSubscriber.cancel();
502+
if (wip.getAndIncrement() != 0) {
503+
return;
454504
}
505+
506+
// need to send frame only if RequestChannelFrame was sent
455507
if (receivers.remove(streamId, receiver)) {
456508
sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId));
457-
upstreamSubscriber.cancel();
458509
}
459-
})
460-
.doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER);
510+
});
461511
}
462512

463513
private Mono<Void> handleMetadataPush(Payload payload) {

rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,34 @@ static ByteBuf encode(
3737
boolean hasMetadata,
3838
ByteBuf data) {
3939

40-
final boolean addData = data != null && data.isReadable();
41-
final boolean addMetadata = hasMetadata && metadata.isReadable();
40+
final boolean addData;
41+
if (data != null) {
42+
if (data.isReadable()) {
43+
addData = true;
44+
} else {
45+
// even though there is nothing to read, we still have to release here since nobody else
46+
// going to do soo
47+
data.release();
48+
addData = false;
49+
}
50+
} else {
51+
addData = false;
52+
}
53+
54+
final boolean addMetadata;
55+
if (hasMetadata) {
56+
if (metadata.isReadable()) {
57+
addMetadata = true;
58+
} else {
59+
// even though there is nothing to read, we still have to release here since nobody else
60+
// going to do soo
61+
metadata.release();
62+
addMetadata = false;
63+
}
64+
} else {
65+
// has no metadata means it is null, thus no need to release anything
66+
addMetadata = false;
67+
}
4268

4369
if (hasMetadata) {
4470
int length = metadata.readableBytes();

rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ public static ByteBuf encode(
1414
@Nullable final ByteBuf metadata) {
1515

1616
final boolean hasMetadata = metadata != null;
17-
final boolean addMetadata = hasMetadata && metadata.isReadable();
1817

1918
int flags = 0;
2019

@@ -27,6 +26,21 @@ public static ByteBuf encode(
2726
.writeInt(ttl)
2827
.writeInt(numRequests);
2928

29+
final boolean addMetadata;
30+
if (hasMetadata) {
31+
if (metadata.isReadable()) {
32+
addMetadata = true;
33+
} else {
34+
// even though there is nothing to read, we still have to release here since nobody else
35+
// going to do soo
36+
metadata.release();
37+
addMetadata = false;
38+
}
39+
} else {
40+
// has no metadata means it is null, thus no need to release anything
41+
addMetadata = false;
42+
}
43+
3044
if (addMetadata) {
3145
return allocator.compositeBuffer(2).addComponents(true, header, metadata);
3246
} else {

rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameFlyweight.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@
22

33
import io.netty.buffer.ByteBuf;
44
import io.netty.buffer.ByteBufAllocator;
5+
import io.netty.util.IllegalReferenceCountException;
56
import io.rsocket.Payload;
67

78
public class MetadataPushFrameFlyweight {
89

910
public static ByteBuf encodeReleasingPayload(ByteBufAllocator allocator, Payload payload) {
1011
final ByteBuf metadata = payload.metadata().retain();
11-
payload.release();
12+
// releasing payload safely since it can be already released wheres we have to release retained
13+
// data and metadata as well
14+
try {
15+
payload.release();
16+
} catch (IllegalReferenceCountException e) {
17+
metadata.release();
18+
throw e;
19+
}
1220
return encode(allocator, metadata);
1321
}
1422

rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.netty.buffer.ByteBuf;
44
import io.netty.buffer.ByteBufAllocator;
5+
import io.netty.util.IllegalReferenceCountException;
56
import io.rsocket.Payload;
67

78
public class PayloadFrameFlyweight {
@@ -23,11 +24,31 @@ public static ByteBuf encodeNextCompleteReleasingPayload(
2324
static ByteBuf encodeReleasingPayload(
2425
ByteBufAllocator allocator, int streamId, boolean complete, Payload payload) {
2526

26-
final boolean hasMetadata = payload.hasMetadata();
27+
// if refCnt exceptions throws here it is safe to do no-op
28+
boolean hasMetadata = payload.hasMetadata();
29+
// if refCnt exceptions throws here it is safe to do no-op still
2730
final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null;
28-
final ByteBuf data = payload.data().retain();
29-
30-
payload.release();
31+
final ByteBuf data;
32+
// retaining data safely. May throw either NPE or RefCntE
33+
try {
34+
data = payload.data().retain();
35+
} catch (IllegalReferenceCountException | NullPointerException e) {
36+
if (hasMetadata) {
37+
metadata.release();
38+
}
39+
throw e;
40+
}
41+
// releasing payload safely since it can be already released wheres we have to release retained
42+
// data and metadata as well
43+
try {
44+
payload.release();
45+
} catch (IllegalReferenceCountException e) {
46+
data.release();
47+
if (hasMetadata) {
48+
metadata.release();
49+
}
50+
throw e;
51+
}
3152

3253
return encode(allocator, streamId, false, complete, true, metadata, data);
3354
}

0 commit comments

Comments
 (0)