-
Notifications
You must be signed in to change notification settings - Fork 1k
Fix a bug where the trailers of a streaming response was dropped by RetryingClient #6213
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
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 | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -39,6 +39,7 @@ | |||||||
| import com.linecorp.armeria.common.stream.StreamMessage; | ||||||||
| import com.linecorp.armeria.common.stream.SubscriptionOption; | ||||||||
| import com.linecorp.armeria.common.util.EventLoopCheckingFuture; | ||||||||
| import com.linecorp.armeria.common.util.UnmodifiableFuture; | ||||||||
|
|
||||||||
| import io.netty.util.concurrent.EventExecutor; | ||||||||
|
|
||||||||
|
|
@@ -50,19 +51,46 @@ public final class SurroundingPublisher<T> implements StreamMessage<T> { | |||||||
| private static final AtomicIntegerFieldUpdater<SurroundingPublisher> subscribedUpdater = | ||||||||
| AtomicIntegerFieldUpdater.newUpdater(SurroundingPublisher.class, "subscribed"); | ||||||||
|
|
||||||||
| public static <T> SurroundingPublisher<T> of(@Nullable T head, Publisher<? extends T> publisher, T tail) { | ||||||||
| // The tail is ignored when the cause is not null. | ||||||||
| return of(head, publisher, cause -> cause != null ? null : tail); | ||||||||
| } | ||||||||
|
|
||||||||
| public static <T> SurroundingPublisher<T> of( | ||||||||
| @Nullable T head, Publisher<? extends T> publisher, | ||||||||
| Function<@Nullable Throwable, ? extends @Nullable T> finalizer) { | ||||||||
| requireNonNull(finalizer, "finalizer"); | ||||||||
| final Function<@Nullable Throwable, @Nullable CompletableFuture<T>> asyncFinalizer = cause -> { | ||||||||
| final T tail = finalizer.apply(cause); | ||||||||
| if (tail == null) { | ||||||||
| return null; | ||||||||
| } else { | ||||||||
| return UnmodifiableFuture.completedFuture(tail); | ||||||||
| } | ||||||||
| }; | ||||||||
| return new SurroundingPublisher<>(head, publisher, asyncFinalizer); | ||||||||
| } | ||||||||
|
|
||||||||
| public static <T> SurroundingPublisher<T> of(@Nullable T head, Publisher<? extends T> publisher, | ||||||||
| CompletableFuture<? extends @Nullable T> tail) { | ||||||||
| // The tail is ignored when the cause is not null. | ||||||||
| return new SurroundingPublisher<>(head, publisher, cause -> cause != null ? null : tail); | ||||||||
| } | ||||||||
|
|
||||||||
| @Nullable | ||||||||
| private final T head; | ||||||||
| private final StreamMessage<T> publisher; | ||||||||
| private final Function<@Nullable Throwable, ? extends @Nullable T> finalizer; | ||||||||
| private final Function<@Nullable Throwable, ? extends @Nullable CompletableFuture<T>> finalizer; | ||||||||
|
|
||||||||
| private volatile int subscribed; | ||||||||
| private final CompletableFuture<Void> completionFuture = new EventLoopCheckingFuture<>(); | ||||||||
|
|
||||||||
| @Nullable | ||||||||
| private volatile SurroundingSubscriber<T> surroundingSubscriber; | ||||||||
|
|
||||||||
| public SurroundingPublisher(@Nullable T head, Publisher<? extends T> publisher, | ||||||||
| Function<@Nullable Throwable, ? extends @Nullable T> finalizer) { | ||||||||
| SurroundingPublisher(@Nullable T head, Publisher<? extends T> publisher, | ||||||||
| Function<@Nullable Throwable, | ||||||||
| ? extends @Nullable CompletableFuture<? extends T>> finalizer) { | ||||||||
| requireNonNull(publisher, "publisher"); | ||||||||
| requireNonNull(finalizer, "finalizer"); | ||||||||
| this.head = head; | ||||||||
|
|
@@ -72,7 +100,8 @@ public SurroundingPublisher(@Nullable T head, Publisher<? extends T> publisher, | |||||||
| } else { | ||||||||
| this.publisher = new PublisherBasedStreamMessage<>(publisher); | ||||||||
| } | ||||||||
| this.finalizer = finalizer; | ||||||||
| //noinspection unchecked | ||||||||
| this.finalizer = (Function<Throwable, ? extends CompletableFuture<T>>) finalizer; | ||||||||
| } | ||||||||
|
|
||||||||
| @Override | ||||||||
|
|
@@ -190,7 +219,7 @@ enum State { | |||||||
| @Nullable | ||||||||
| private T head; | ||||||||
| private final StreamMessage<T> publisher; | ||||||||
| private final Function<@Nullable Throwable, ? extends @Nullable T> finalizer; | ||||||||
| private final Function<@Nullable Throwable, ? extends @Nullable CompletableFuture<T>> finalizer; | ||||||||
|
|
||||||||
| private Subscriber<? super T> downstream; | ||||||||
| private final EventExecutor executor; | ||||||||
|
|
@@ -206,7 +235,7 @@ enum State { | |||||||
| private final SubscriptionOption[] options; | ||||||||
|
|
||||||||
| SurroundingSubscriber(@Nullable T head, StreamMessage<T> publisher, | ||||||||
| Function<@Nullable Throwable, ? extends @Nullable T> finalizer, | ||||||||
| Function<@Nullable Throwable, ? extends @Nullable CompletableFuture<T>> finalizer, | ||||||||
| Subscriber<? super T> downstream, EventExecutor executor, | ||||||||
| CompletableFuture<Void> completionFuture, SubscriptionOption... options) { | ||||||||
| requireNonNull(publisher, "publisher"); | ||||||||
|
|
@@ -305,9 +334,9 @@ private void sendTail() { | |||||||
| } | ||||||||
|
|
||||||||
| private void finalize(@Nullable Throwable cause) { | ||||||||
| final T tail; | ||||||||
| final CompletableFuture<T> tailFuture; | ||||||||
| try { | ||||||||
| tail = finalizer.apply(cause); | ||||||||
| tailFuture = finalizer.apply(cause); | ||||||||
| } catch (Throwable ex) { | ||||||||
| if (cause != null) { | ||||||||
| logger.warn("Unexpected exception from finalizer:", ex); | ||||||||
|
|
@@ -318,11 +347,28 @@ private void finalize(@Nullable Throwable cause) { | |||||||
| return; | ||||||||
| } | ||||||||
|
|
||||||||
| if (tail == null) { | ||||||||
| if (tailFuture == null) { | ||||||||
| // Immediately close the stream if the finalizer returns null. | ||||||||
| close0(cause); | ||||||||
| } else { | ||||||||
| downstream.onNext(tail); | ||||||||
| tailFuture.handle((tail, cause0) -> { | ||||||||
| if (executor.inEventLoop()) { | ||||||||
| handleTail(tail, cause0); | ||||||||
| } else { | ||||||||
| executor.execute(() -> handleTail(tail, cause0)); | ||||||||
| } | ||||||||
| return null; | ||||||||
| }); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| private void handleTail(@Nullable T tail, @Nullable Throwable cause) { | ||||||||
|
Contributor
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 seems like the
Contributor
Author
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.
armeria/core/src/main/java/com/linecorp/armeria/common/HttpResponse.java Lines 537 to 539 in 54730fe
Contributor
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. Got it. I missed that. 😓 |
||||||||
| if (cause != null) { | ||||||||
| close0(cause); | ||||||||
| } else { | ||||||||
| if (tail != null) { | ||||||||
| downstream.onNext(tail); | ||||||||
| } | ||||||||
| close0(null); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
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.
Understood that the throwable can be null, the CF can be null, and the tail handeld by the CF can also be null.
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.
Correct. All can be null.