-
Notifications
You must be signed in to change notification settings - Fork 930
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix a leak in HttpEncodedResponse
#5858
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -16,27 +16,21 @@ | |||||||||||||||||||||
|
||||||||||||||||||||||
package com.linecorp.armeria.common.stream; | ||||||||||||||||||||||
|
||||||||||||||||||||||
import static com.linecorp.armeria.internal.common.stream.InternalStreamMessageUtil.EMPTY_OPTIONS; | ||||||||||||||||||||||
import static com.linecorp.armeria.internal.common.stream.InternalStreamMessageUtil.POOLED_OBJECTS; | ||||||||||||||||||||||
import static com.linecorp.armeria.internal.common.stream.InternalStreamMessageUtil.containsNotifyCancellation; | ||||||||||||||||||||||
import static com.linecorp.armeria.internal.common.stream.InternalStreamMessageUtil.containsWithPooledObjects; | ||||||||||||||||||||||
import static com.linecorp.armeria.internal.common.stream.InternalStreamMessageUtil.toSubscriptionOptions; | ||||||||||||||||||||||
import static java.util.Objects.requireNonNull; | ||||||||||||||||||||||
|
||||||||||||||||||||||
import java.util.List; | ||||||||||||||||||||||
import java.util.concurrent.CompletableFuture; | ||||||||||||||||||||||
|
||||||||||||||||||||||
import org.reactivestreams.Subscriber; | ||||||||||||||||||||||
import org.reactivestreams.Subscription; | ||||||||||||||||||||||
import org.slf4j.Logger; | ||||||||||||||||||||||
import org.slf4j.LoggerFactory; | ||||||||||||||||||||||
|
||||||||||||||||||||||
import com.google.common.collect.ImmutableList; | ||||||||||||||||||||||
|
||||||||||||||||||||||
import com.linecorp.armeria.common.HttpData; | ||||||||||||||||||||||
import com.linecorp.armeria.common.annotation.Nullable; | ||||||||||||||||||||||
import com.linecorp.armeria.common.annotation.UnstableApi; | ||||||||||||||||||||||
import com.linecorp.armeria.common.util.Exceptions; | ||||||||||||||||||||||
import com.linecorp.armeria.internal.common.stream.StreamMessageUtil; | ||||||||||||||||||||||
import com.linecorp.armeria.unsafe.PooledObjects; | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
@@ -136,79 +130,6 @@ public final CompletableFuture<Void> whenComplete() { | |||||||||||||||||||||
return completionFuture; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
@Override | ||||||||||||||||||||||
public CompletableFuture<List<U>> collect(EventExecutor executor, SubscriptionOption... options) { | ||||||||||||||||||||||
final SubscriptionOption[] filterOptions = filterSupportsPooledObjects ? POOLED_OBJECTS : EMPTY_OPTIONS; | ||||||||||||||||||||||
return upstream.collect(executor, filterOptions).handle((result, cause) -> { | ||||||||||||||||||||||
// CollectingSubscriberAndSubscription just captures cancel(), onComplete(), and onError() signals | ||||||||||||||||||||||
// from the subclass of FilteredStreamMessage. So we need to follow regular Reactive Streams | ||||||||||||||||||||||
// specifications. | ||||||||||||||||||||||
final CollectingSubscriberAndSubscription<U> subscriberAndSubscription = | ||||||||||||||||||||||
new CollectingSubscriberAndSubscription<>(); | ||||||||||||||||||||||
beforeSubscribe(subscriberAndSubscription, subscriberAndSubscription); | ||||||||||||||||||||||
if (cause != null) { | ||||||||||||||||||||||
beforeError(subscriberAndSubscription, cause); | ||||||||||||||||||||||
completionFuture.completeExceptionally(cause); | ||||||||||||||||||||||
return Exceptions.throwUnsafely(cause); | ||||||||||||||||||||||
} else { | ||||||||||||||||||||||
Throwable abortCause = null; | ||||||||||||||||||||||
final ImmutableList.Builder<U> builder = ImmutableList.builderWithExpectedSize(result.size()); | ||||||||||||||||||||||
final boolean withPooledObjects = containsWithPooledObjects(options); | ||||||||||||||||||||||
for (T t : result) { | ||||||||||||||||||||||
if (abortCause != null) { | ||||||||||||||||||||||
// This StreamMessage was aborted already. However, we need to release the remaining | ||||||||||||||||||||||
// objects in result. | ||||||||||||||||||||||
StreamMessageUtil.closeOrAbort(t, abortCause); | ||||||||||||||||||||||
continue; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
try { | ||||||||||||||||||||||
U filtered = filter(t); | ||||||||||||||||||||||
|
||||||||||||||||||||||
if (subscriberAndSubscription.completed || subscriberAndSubscription.cause != null || | ||||||||||||||||||||||
subscriberAndSubscription.cancelled) { | ||||||||||||||||||||||
if (subscriberAndSubscription.cause != null) { | ||||||||||||||||||||||
abortCause = cause; | ||||||||||||||||||||||
} else { | ||||||||||||||||||||||
abortCause = CancelledSubscriptionException.get(); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
StreamMessageUtil.closeOrAbort(filtered, abortCause); | ||||||||||||||||||||||
} else { | ||||||||||||||||||||||
requireNonNull(filtered, "filter() returned null"); | ||||||||||||||||||||||
if (!withPooledObjects) { | ||||||||||||||||||||||
filtered = PooledObjects.copyAndClose(filtered); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
builder.add(filtered); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} catch (Throwable ex) { | ||||||||||||||||||||||
// Failed to filter the object. | ||||||||||||||||||||||
StreamMessageUtil.closeOrAbort(t, abortCause); | ||||||||||||||||||||||
abortCause = ex; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
final List<U> elements = builder.build(); | ||||||||||||||||||||||
if (abortCause != null && !(abortCause instanceof CancelledSubscriptionException)) { | ||||||||||||||||||||||
// The stream was aborted with an unsafe exception. | ||||||||||||||||||||||
for (U element : elements) { | ||||||||||||||||||||||
StreamMessageUtil.closeOrAbort(element, abortCause); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
completionFuture.completeExceptionally(abortCause); | ||||||||||||||||||||||
return Exceptions.throwUnsafely(abortCause); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
try { | ||||||||||||||||||||||
beforeComplete(subscriberAndSubscription); | ||||||||||||||||||||||
completionFuture.complete(null); | ||||||||||||||||||||||
} catch (Exception ex) { | ||||||||||||||||||||||
completionFuture.completeExceptionally(ex); | ||||||||||||||||||||||
throw ex; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
return elements; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
}); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
@Override | ||||||||||||||||||||||
public final void subscribe(Subscriber<? super U> subscriber, EventExecutor executor) { | ||||||||||||||||||||||
subscribe(subscriber, executor, false, false); | ||||||||||||||||||||||
|
@@ -298,17 +219,21 @@ public void onNext(T o) { | |||||||||||||||||||||
try { | ||||||||||||||||||||||
filtered = filter(o); | ||||||||||||||||||||||
} catch (Throwable ex) { | ||||||||||||||||||||||
StreamMessageUtil.closeOrAbort(o); | ||||||||||||||||||||||
// onError(ex) should be called before upstream.cancel() to deliver the cause to downstream. | ||||||||||||||||||||||
// upstream.cancel() and make downstream closed with CancelledSubscriptionException | ||||||||||||||||||||||
// before sending the actual cause. | ||||||||||||||||||||||
// upstream.cancel() may close downstream with CancelledSubscriptionException before sending | ||||||||||||||||||||||
// the actual cause. | ||||||||||||||||||||||
onError(ex); | ||||||||||||||||||||||
|
||||||||||||||||||||||
assert upstream != null; | ||||||||||||||||||||||
upstream.cancel(); | ||||||||||||||||||||||
return; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
if (completed) { | ||||||||||||||||||||||
// onError(Throwable) or onComplete() has been called in filter(). | ||||||||||||||||||||||
StreamMessageUtil.closeOrAbort(filtered); | ||||||||||||||||||||||
return; | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't understånd this change 😅 Shouldn't the last filtered object be also passed downstream? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should not publish items if after calling Otherwise, did you mean something else? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I imagined armeria/core/src/main/java/com/linecorp/armeria/common/stream/FilteredStreamMessage.java Lines 268 to 270 in 6fae642
where armeria/core/src/main/java/com/linecorp/armeria/server/encoding/HttpEncodedResponse.java Line 163 in 6fae642
I thought this was the scenario this PR was trying to address There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I fixed this to pass armeria/core/src/test/java/com/linecorp/armeria/common/stream/StreamMessageCollectingTest.java Lines 185 to 190 in c7aca10
It's impossible to know what all subclasses do. So we may need to prevent @Override
public void onNext(T o) {
if (complete) {
return;
}
U filtered;
try {
filtered = filter(o);
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By the way, only the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I just realized that the downstream There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not nice that a |
||||||||||||||||||||||
} | ||||||||||||||||||||||
if (!subscribedWithPooledObjects) { | ||||||||||||||||||||||
filtered = PooledObjects.copyAndClose(filtered); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
@@ -351,42 +276,4 @@ public void onComplete() { | |||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
private static final class CollectingSubscriberAndSubscription<T> implements Subscriber<T>, Subscription { | ||||||||||||||||||||||
|
||||||||||||||||||||||
private boolean completed; | ||||||||||||||||||||||
private boolean cancelled; | ||||||||||||||||||||||
@Nullable | ||||||||||||||||||||||
private Throwable cause; | ||||||||||||||||||||||
|
||||||||||||||||||||||
@Override | ||||||||||||||||||||||
public void onSubscribe(Subscription s) {} | ||||||||||||||||||||||
|
||||||||||||||||||||||
@Override | ||||||||||||||||||||||
public void onNext(T o) {} | ||||||||||||||||||||||
|
||||||||||||||||||||||
@Override | ||||||||||||||||||||||
public void onError(Throwable t) { | ||||||||||||||||||||||
if (completed) { | ||||||||||||||||||||||
return; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
cause = t; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
@Override | ||||||||||||||||||||||
public void onComplete() { | ||||||||||||||||||||||
if (cause != null) { | ||||||||||||||||||||||
return; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
completed = true; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
@Override | ||||||||||||||||||||||
public void request(long n) {} | ||||||||||||||||||||||
|
||||||||||||||||||||||
@Override | ||||||||||||||||||||||
public void cancel() { | ||||||||||||||||||||||
cancelled = true; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -295,8 +295,8 @@ public void onNext(Object item) { | |
if (result != null && item != result) { | ||
StreamMessageUtil.closeOrAbort(result, ex); | ||
} | ||
upstream.cancel(); | ||
onError(ex); | ||
upstream.cancel(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So in general, do we always cancel the upstream before propagating the error downstream now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we cancel the upstream first, So I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this change is unrelated to the leak, am I understanding correctly? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right. I found the bug while fixing broken tests after removing |
||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -20,6 +20,7 @@ | |||
import static com.google.common.collect.ImmutableList.toImmutableList; | ||||
import static com.linecorp.armeria.common.stream.StreamMessageUtil.createStreamMessageFrom; | ||||
import static com.linecorp.armeria.internal.common.stream.InternalStreamMessageUtil.EMPTY_OPTIONS; | ||||
import static com.linecorp.armeria.internal.common.stream.InternalStreamMessageUtil.containsNotifyCancellation; | ||||
import static java.util.Objects.requireNonNull; | ||||
|
||||
import java.io.File; | ||||
|
@@ -43,6 +44,7 @@ | |||
|
||||
import com.google.common.collect.ImmutableList; | ||||
import com.google.common.collect.Iterables; | ||||
import com.google.common.collect.ObjectArrays; | ||||
|
||||
import com.linecorp.armeria.common.CommonPools; | ||||
import com.linecorp.armeria.common.HttpData; | ||||
|
@@ -752,6 +754,11 @@ default CompletableFuture<List<T>> collect(EventExecutor executor, SubscriptionO | |||
requireNonNull(executor, "executor"); | ||||
requireNonNull(options, "options"); | ||||
final StreamMessageCollector<T> collector = new StreamMessageCollector<>(options); | ||||
if (!containsNotifyCancellation(options)) { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before setting this PR as ready for review, it would be easier to review if you explain why this change is needed in the PR description There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You can reproduce it by disabling this block and then running it below. armeria/core/src/test/java/com/linecorp/armeria/common/stream/StreamMessageCollectingTest.java Line 168 in a870edf
I believe it would make more sense to specify There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For context, previously, the custom There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, I understood the issue now. Agree with the approach 👍 |
||||
// Make the return CompletableFuture completed exceptionally if the stream is cancelled while | ||||
// collecting the elements. | ||||
options = ObjectArrays.concat(options, SubscriptionOption.NOTIFY_CANCELLATION); | ||||
} | ||||
subscribe(collector, executor, options); | ||||
return collector.collect(); | ||||
} | ||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to call
StreamMessageUtil.closeOrAbort(filtered);
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't
filtered
null when we reach here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For context, I found a case where
o
was double-released when a maximum length was exceeded. CallingStreamMessageUtil.closeOrAbort(o)
didn't make sense since the ownership has been transferred tofilter()
method.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is correct. 👍 The previous logic was wrong.