Skip to content
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

Request content preview isn't produced if decorate a service with ThrottlingService #3816

Closed
0x1306e6d opened this issue Sep 7, 2021 · 7 comments · Fixed by #3884
Closed
Labels
Milestone

Comments

@0x1306e6d
Copy link
Contributor

0x1306e6d commented Sep 7, 2021

I found that RequestLog#requestContentPreview returns null when I decorate a service using ContentPreviewingService and ThrottlingService.

The request content preview is not produced when aggregate a service which is decorated with following structure:

.decorator(((delegate, ctx, req) -> HttpResponse.from(
     completedFuture(null).handleAsync((ignored, cause) -> {
         try {
             return delegate.serve(ctx, req);
         } catch (Exception e) {
             return Exceptions.throwUnsafely(e);
         }
     }, ctx.eventLoop()))))

Following test can reproduce:

class RequestContentPreviewTest {

    private static final AtomicReference<ServiceRequestContext> ctxCaptor = new AtomicReference<>();

    @RegisterExtension
    static ServerExtension server = new ServerExtension() {
        @Override
        protected void configure(ServerBuilder builder) {
            builder.service("/", (ctx, req) -> {
                       ctxCaptor.set(ctx);
                       return HttpResponse.from(req.aggregate().thenApply(
                               aggregated -> HttpResponse.of("Hello, " + aggregated.contentUtf8() + "!")));
                   })
                   .decorator(LoggingService.builder()
                                            .requestLogLevel(LogLevel.INFO)
                                            .successfulResponseLogLevel(LogLevel.INFO)
                                            .failureResponseLogLevel(LogLevel.WARN)
                                            .newDecorator())
                   .decorator(ContentPreviewingService.newDecorator(Integer.MAX_VALUE))
                   .decorator(((delegate, ctx, req) -> HttpResponse.from(
                           completedFuture(null).handleAsync((ignored, cause) -> {
                               try {
                                   return delegate.serve(ctx, req);
                               } catch (Exception e) {
                                   return Exceptions.throwUnsafely(e);
                               }
                           }, ctx.eventLoop()))));
        }
    };

    @Test
    void requestContentPreviewH1C() {
        final WebClient client = WebClient.of(SessionProtocol.H1C,
                                              Endpoint.of("127.0.0.1", server.httpPort()));
        final RequestHeaders headers = RequestHeaders.of(HttpMethod.POST, "/",
                                                         HttpHeaderNames.CONTENT_TYPE, "text/plain");
        final AggregatedHttpResponse res = client.execute(headers, HttpData.ofUtf8("Armeria"))
                                                 .aggregate()
                                                 .join();
        assertThat(res.contentUtf8()).isEqualTo("Hello, Armeria!");

        final ServiceRequestContext ctx = ctxCaptor.get();
        final RequestLog log = ctx.log().whenComplete().join();
        assertThat(log.responseContentPreview()).isEqualTo("Hello, Armeria!");
        // expected: "Armeria"
        // but was: null
        // org.opentest4j.AssertionFailedError:
        // expected: "Hello, World!"
        assertThat(log.requestContentPreview()).isEqualTo("Armeria");
    }

    @Test
    void requestContentPreviewHTTP() {
        final WebClient client = WebClient.of(SessionProtocol.HTTP,
                                              Endpoint.of("127.0.0.1", server.httpPort()));
        final RequestHeaders headers = RequestHeaders.of(HttpMethod.POST, "/",
                                                         HttpHeaderNames.CONTENT_TYPE, "text/plain");
        final AggregatedHttpResponse res = client.execute(headers, HttpData.ofUtf8("Armeria"))
                                                 .aggregate()
                                                 .join();
        assertThat(res.contentUtf8()).isEqualTo("Hello, Armeria!");

        final ServiceRequestContext ctx = ctxCaptor.get();
        final RequestLog log = ctx.log().whenComplete().join();
        assertThat(log.responseContentPreview()).isEqualTo("Hello, Armeria!");
        assertThat(log.requestContentPreview()).isEqualTo("Armeria");
    }
}

logging H1C:

02:08:33.724 [armeria-common-worker-nio-2-2] INFO  c.l.a.server.logging.LoggingService - [sreqId=64b5968f, chanId=ec5eedd2, raddr=127.0.0.1:54446, laddr=127.0.0.1:54445][h1c://al01540922.local/#POST] Request: {startTime=2021-09-08T17:08:33.696Z(1631120913696000), length=7B, duration=24472µs(24472956ns), scheme=none+h1c, name=POST, headers=[:method=POST, :path=/, :scheme=http, host=127.0.0.1:54445, content-type=text/plain, user-agent=armeria/1.11.1-SNAPSHOT, content-length=7]}
02:08:33.741 [armeria-common-worker-nio-2-2] INFO  c.l.a.server.logging.LoggingService - [sreqId=64b5968f, chanId=ec5eedd2, raddr=127.0.0.1:54446, laddr=127.0.0.1:54445][h1c://al01540922.local/#POST] Response: {startTime=2021-09-08T17:08:33.737Z(1631120913737000), length=15B, duration=3305µs(3305059ns), totalDuration=43624µs(43624769ns), headers=[:status=200, content-type=text/plain; charset=utf-8, content-length=15], contentPreview=Hello, Armeria!}

logging H2C:

02:08:33.884 [armeria-common-worker-nio-2-4] INFO  c.l.a.server.logging.LoggingService - [sreqId=d4e30197, chanId=1d1d7144, raddr=127.0.0.1:54447, laddr=127.0.0.1:54445][h2c://al01540922.local/#POST] Request: {startTime=2021-09-08T17:08:33.882Z(1631120913882000), length=7B, duration=1976µs(1976048ns), scheme=none+h2c, name=POST, headers=[:method=POST, :path=/, :scheme=http, :authority=127.0.0.1:54445, content-type=text/plain, user-agent=armeria/1.11.1-SNAPSHOT, content-length=7], contentPreview=Armeria}
02:08:33.886 [armeria-common-worker-nio-2-4] INFO  c.l.a.server.logging.LoggingService - [sreqId=d4e30197, chanId=1d1d7144, raddr=127.0.0.1:54447, laddr=127.0.0.1:54445][h2c://al01540922.local/#POST] Response: {startTime=2021-09-08T17:08:33.884Z(1631120913884000), length=15B, duration=1648µs(1648011ns), totalDuration=3309µs(3309007ns), headers=[:status=200, content-type=text/plain; charset=utf-8, content-length=15], contentPreview=Hello, Armeria!}

It works well if not decorated by the last one.

What I found is that


is called before

And this is because a CloseEvent from Http1RequestDecoder is already in queue before collect.

@0x1306e6d 0x1306e6d changed the title Request content preview isn't produced if decorated with ThrottlingService Request content preview isn't produced if decorate TomcatService with ThrottlingService Sep 7, 2021
@0x1306e6d 0x1306e6d changed the title Request content preview isn't produced if decorate TomcatService with ThrottlingService Request content preview isn't produced if decorate a service with ThrottlingService Sep 8, 2021
@ikhoon ikhoon added the defect label Sep 9, 2021
@ikhoon ikhoon added this to the 1.12.0 milestone Sep 9, 2021
@ikhoon
Copy link
Contributor

ikhoon commented Sep 9, 2021

It seems like the req.whenComplete() was completed before filteredHttpRequest.whenComplete().
The following callback is used to detect the abortion of the upstream.


I think we can safely change req.whenComplete().handle() to req.whenComplete().exceptionally() so that the logBuilder.requestContentPreview(null) is set only when the request is completed exceptionally.
Otherwise, FilteredHttpRequest will set logBuilder.requestContentPreview().

Are you sending a PR? 😆

@ikhoon
Copy link
Contributor

ikhoon commented Sep 9, 2021

On second thought, it might be better to fix FilterStreamMessage to be completed when the upstream is exceptionally completed.

public abstract class FilteredStreamMessage<T, U> implements StreamMessage<U> {
    // Set true when being collected or subscribed
    private volatile boolean subscribed;

    protected FilteredStreamMessage(StreamMessage<T> upstream, boolean withPooledObjects) {
        ...
        upstream.whenComplete().exceptionally(cause -> {
            if (!subscribed) {
                // complete `completionFuture` with the cause
            }
        }
    }

, and could remove req.whenComplete().handle() block in ContentPreviewingUtil.

@0x1306e6d
Copy link
Contributor Author

0x1306e6d commented Sep 9, 2021

Thanks @ikhoon!

Second idea looks good. Let me prepare PR.

I have a question. I found that when a HttpResponse is deferred, handleCloseEvent can be called before delegated service is served in HTTP/1. Maybe is it ok?

@ikhoon
Copy link
Contributor

ikhoon commented Sep 9, 2021

I found that when a HttpResponse is deferred, handleCloseEvent can be called before delegated service is served in HTTP/1. Maybe is it ok?

When I locally tested it, handleCloseEvent seems to be called by req.aggregate() in service.
It should be a bug if handleCloseEvent is called before subscription.

@0x1306e6d
Copy link
Contributor Author

I think I was confused. Please ignore 😅

@minwoox minwoox modified the milestones: 1.12.0, 1.13.0 Sep 30, 2021
@ikhoon ikhoon modified the milestones: 1.13.0, 1.14.0 Oct 14, 2021
@0x1306e6d
Copy link
Contributor Author

@ikhoon, could you check following test?

@Test
void shouldCompleteWhenUpstreamCompletedExceptionally() {
    final EventLoop eventLoop = eventLoopExtension.get();

    final ByteBuf buf = newPooledBuffer();
    final HttpData data = HttpData.wrap(buf).withEndOfStream();
    final DefaultStreamMessage<HttpData> stream = new DefaultStreamMessage<>();
    stream.abort(new RuntimeException());

    final AtomicBoolean completed = new AtomicBoolean();

    final FilteredStreamMessage<HttpData, HttpData> filtered =
            new FilteredStreamMessage<HttpData, HttpData>(stream) {
                @Override
                protected HttpData filter(HttpData obj) {
                    return obj;
                }
            };
    filtered.whenComplete().handle((ignored, cause) -> {
        completed.set(true);
        return null;
    });

    eventLoop.execute(() -> filtered.collect(eventLoop));

    await().untilAtomic(completed, Matchers.is(true));
}

it might be better to fix FilterStreamMessage to be completed when the upstream is exceptionally completed.

I expected that FilteredStreamMessage isn't completed when upstream completed exceptionally, but it is.

So I guess we can simply

remove req.whenComplete().handle() block in ContentPreviewingUtil.

@ikhoon
Copy link
Contributor

ikhoon commented Oct 15, 2021

I expected that FilteredStreamMessage isn't completed when upstream completed exceptionally, but it is.

Good analysis. If upstream is aborted, it is propagated to FilteredStreamMessage.
As like your suggestion, we can safely remove req.whenComplete().handle(). 👍

@trustin trustin modified the milestones: 1.14.0, 1.13.1 Oct 18, 2021
trustin pushed a commit that referenced this issue Oct 20, 2021
Motivation:

 - #3816 

Modifications:

- Remove setting `requestContentPreview` with `null` when upstream `HttpRequest` is completed.

Result:

- Closes #3816
- Request content preview is produced well with deferred decorators like `AuthService` or `ThrottlingService`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants