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

Introduce a way to unwrap RequestContexts #4308

Merged
merged 26 commits into from
Aug 4, 2022

Conversation

jrhee17
Copy link
Contributor

@jrhee17 jrhee17 commented Jun 22, 2022

Motivation:

Armeria has some core logic which assumes that the concrete default implementations of [Client|Service]RequestContext are Default[Client|Service]RequestContext.

e.g. If the ClientRequestContext isn't an instance of DefaultClientRequestContext, then the response timeout may not be respected.

if (ctx instanceof DefaultClientRequestContext) {
final CancellationScheduler responseCancellationScheduler =
((DefaultClientRequestContext) ctx).responseCancellationScheduler();
responseCancellationScheduler.init(
ctx.eventLoop(), newCancellationTask(),
TimeUnit.MILLISECONDS.toNanos(responseTimeoutMillis), /* server */ false);
}

This usage pattern mostly stems from the fact that [Client|Service]RequestContext are public APIs that users are exposed to a lot (especially in decorators). However, Armeria developers are skeptical of adding public methods that are either 1) very specific and used only in core logic 2) have the potential to break behavior if used incorrectly.

Up to now, we have added such methods to the default implementation and checked with instanceof when invoking such methods.

With the introduction of #4232, we are now planning on introducing and extensively utilizing wrapper classes for [Client|Service]RequestContext. However, using wrapper classes means previous assumptions of instanceof Default[Client|Service]RequestContext will be invalid.

In this pull request, I propose that:

  1. RequestContext now extends Unwrappable, allowing users to peel RequestContexts to the intended type.
  2. We expose another interface [Client]RequestContextExtension, which contains "extension" methods for ClientRequestContext, RequestContext. In this interface, we can add methods that Armeria uses internally without worrying about public API exposure.
  3. Default[Client|Service]RequestContext doesn't need to be exposed as a public API. We may simply expose the interface to users and hide the detailed implementation like we do for the Http[Response|Request] series.
    • Note: This move was handled in this PR mainly due to CancellationScheduler. While extracting ClientRequestContextExtension, CancellationScheduler which is an internal class was exposed as the return value of #responseCancellationScheduler. I had the choice of exposing CancellationScheduler, or hiding DefaultClientRequestContext. I chose the latter and also moved DefaultServiceRequestContext for consistency.

Modifications:

  • RequestContext now extends Unwrappable. RequestContextWrapper now extends AbstractUnwrappable
  • Convert usages of instanceof DefaultClientRequestContext to unwrap instead
    • e.g. ctx instanceof DefaultClientRequestContext -> ctx.as(ClientRequestContextExtension.class)
  • RequestContextExtension, ClientRequestContextExtension has been added to an internal package
  • DefaultClientRequestContext, DefaultServiceRequestContext, NonWrappingRequestContext have been moved to an internal package
    • pathWithQuery is moved to ClientUtil and made public
    • ClientThreadLocalState is moved to an internal package and made public
  • Class hierarchy has been modified for RequestContext
    • Now there is an extra layer between ClientRequestContext and DefaultClientRequestContext:
      • ClientRequestContext -> ClientRequestContextExtension -> DefaultClientRequestContext
    • Now there is an extra layer between RequestContext and NonWrappingRequestContext
      • RequestContext -> RequestContextExtension -> NonWrappingRequestContext

Before:
Screen Shot 2022-06-24 at 12 00 49 PM
After:
Screen Shot 2022-06-24 at 12 01 15 PM

Result:

  • Developers may add more utility functions to RequestContext without worrying about public API exposure
  • Armeria tries to unwrap RequestContext instead of simply checking the type.

@codecov
Copy link

codecov bot commented Jun 22, 2022

Codecov Report

Merging #4308 (32d7692) into master (dd31525) will increase coverage by 0.00%.
The diff coverage is 45.00%.

@@            Coverage Diff            @@
##             master    #4308   +/-   ##
=========================================
  Coverage     73.51%   73.51%           
- Complexity    17663    17673   +10     
=========================================
  Files          1504     1504           
  Lines         66100    66112   +12     
  Branches       8338     8341    +3     
=========================================
+ Hits          48594    48603    +9     
- Misses        13292    13294    +2     
- Partials       4214     4215    +1     
Impacted Files Coverage Δ
...rp/armeria/client/ClientRequestContextBuilder.java 80.64% <ø> (ø)
...rp/armeria/client/ClientRequestContextWrapper.java 5.40% <0.00%> (+5.40%) ⬆️
...main/java/com/linecorp/armeria/client/Clients.java 62.50% <ø> (ø)
.../com/linecorp/armeria/client/DefaultWebClient.java 93.10% <ø> (+0.91%) ⬆️
...n/java/com/linecorp/armeria/client/UserClient.java 79.41% <ø> (ø)
...rmeria/internal/client/ClientThreadLocalState.java 80.70% <ø> (ø)
...ria/internal/common/NonWrappingRequestContext.java 79.41% <ø> (ø)
.../internal/server/DefaultServiceRequestContext.java 87.66% <ø> (ø)
...rp/armeria/server/AbstractHttpResponseHandler.java 92.00% <ø> (ø)
.../armeria/server/AggregatedHttpResponseHandler.java 73.68% <ø> (ø)
... and 46 more

Help us with your feedback. Take ten seconds to tell us how you rate us.

@jrhee17 jrhee17 force-pushed the feature/unwrappable-context branch from 9ed815f to b6b53b6 Compare June 22, 2022 10:38
@jrhee17 jrhee17 force-pushed the feature/unwrappable-context branch from b6b53b6 to a4da669 Compare June 22, 2022 13:10
@jrhee17 jrhee17 marked this pull request as ready for review June 24, 2022 03:32
@jrhee17 jrhee17 modified the milestones: 1.18.0, 1.17.0 Jun 24, 2022
Copy link
Contributor

@minwoox minwoox left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! 😄

Copy link
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @jrhee17! 💯

@@ -464,6 +465,11 @@ default ByteBufAllocator alloc() {
@MustBeClosed
SafeCloseable push();

@Override
default RequestContext unwrap() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

master branch has been merged. Let's do the same work for unwrapAll(). 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do the same work for unwrapAll()

Do you mean override the return type for unwrapAll?
I thought it's difficult to do this with the current constraints provided by Unwrappable 😅

ref: #4338 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we safely cast the return type to RequestContext?

@Override
default RequestContext unwrapAll() {
    return (RequestContext) Unwrappable.super.unwrapAll();
}

Copy link
Contributor Author

@jrhee17 jrhee17 Aug 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I guess I should've made my comment on the other PR clearer 😅

To be precise, as you said we can override the return type as RequestContext for objects that are "unwrappable".

However, I'm unsure how the API should be designed for objects that wrap an Unwrappable (i.e. via AbstractUnwrappable)

  1. If AbstractUnwrappable#unwrapAll returns an Object:
    In this case AbstractUnwrappable#unwrapAll returns an Object whereas RequestContext#unwrapAll returns a RequestContext, which causes a return type collision.
    e.g.
'unwrapAll()' in 'com.linecorp.armeria.common.util.AbstractUnwrappable' clashes with 'unwrapAll()' in 'com.linecorp.armeria.common.RequestContext'; attempting to use incompatible return type
  1. If AbstractUnwrappable#unwrapAll returns T:
    I'm not sure if there's type safe way to know the innermost type of a wrappable.
final Foo foo = new Foo();
final Bar<Foo> bar = new Bar<>(foo);
final Qux<Bar<Foo>> qux = new Qux<>(bar);

I guess it's not too late to modify to a type unsafe API though 😅
e.g.

<U extends Unwrappable> U unwrapAll()

ref: #4338 (comment)

Let me know if you have a better idea though 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'unwrapAll()' in 'com.linecorp.armeria.common.util.AbstractUnwrappable' clashes with 'unwrapAll()' in 'com.linecorp.armeria.common.RequestContext'; attempting to use incompatible return type

The error seems to happen in RequestContextWrapper. How about overriding the return type of unwrapAll() in RequestContextWrapper?
I don't see that we need to change the return type of AbstractUnwrappable#unwrapAll.
The subtype of AbstractUnwrappable or Unwrappable freely overrides the return type if necessary.

public abstract class RequestContextWrapper<T extends RequestContext>
        extends AbstractUnwrappable<T> implements RequestContext {

    @Override
    public RequestContext unwrapAll() {
        return (RequestContext) super.unwrapAll();
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I think you're right. I guess we can override the return type for wrapper types only. Done!

@ikhoon
Copy link
Contributor

ikhoon commented Aug 4, 2022

Thanks, @jrhee17! You paid off our technical debt. 😆
Let me merge this PR when CI builds finish.

@ikhoon ikhoon merged commit aa3c6e1 into line:master Aug 4, 2022
heowc pushed a commit to heowc/armeria that referenced this pull request Sep 24, 2022
Motivation:

Armeria has some core logic which assumes that the concrete default implementations of `[Client|Service]RequestContext` are `Default[Client|Service]RequestContext`.

e.g. If the `ClientRequestContext` isn't an instance of `DefaultClientRequestContext`, then the response timeout may not be respected.
https://github.com/line/armeria/blob/5edaef039abfd047f152a63cde38b46094c8353f/core/src/main/java/com/linecorp/armeria/client/HttpResponseDecoder.java#L401-L407

This usage pattern mostly stems from the fact that `[Client|Service]RequestContext` are public APIs that users are exposed to a lot (especially in decorators). However, Armeria developers are skeptical of adding public methods that are either 1) very specific and used only in core logic 2) have the potential to break behavior if used incorrectly.

Up to now, we have added such methods to the default implementation and checked with `instanceof` when invoking such methods.

With the introduction of line#4232, we are now planning on introducing and extensively utilizing wrapper classes for `[Client|Service]RequestContext`. However, using wrapper classes means previous assumptions of `instanceof Default[Client|Service]RequestContext` will be invalid.

In this pull request, I propose that:

1) `RequestContext` now extends `Unwrappable`, allowing users to peel `RequestContext`s to the intended type.
2) We expose another interface `[Client]RequestContextExtension`, which contains "extension" methods for `ClientRequestContext`, `RequestContext`. In this interface, we can add methods that Armeria uses internally without worrying about public API exposure.
3) `Default[Client|Service]RequestContext` doesn't need to be exposed as a public API. We may simply expose the interface to users and hide the detailed implementation like we do for the `Http[Response|Request]` series.
    - Note: This move was handled in this PR mainly due to `CancellationScheduler`. While extracting `ClientRequestContextExtension`, `CancellationScheduler` which is an internal class was exposed as the return value of `#responseCancellationScheduler`. I had the choice of exposing `CancellationScheduler`, or hiding `DefaultClientRequestContext`. I chose the latter and also moved `DefaultServiceRequestContext` for consistency.

Modifications:

- `RequestContext` now extends `Unwrappable`. `RequestContextWrapper` now extends `AbstractUnwrappable`
- Convert usages of `instanceof DefaultClientRequestContext` to unwrap instead
    - e.g. `ctx instanceof DefaultClientRequestContext` -> `ctx.as(ClientRequestContextExtension.class)`
- `RequestContextExtension`, `ClientRequestContextExtension` has been added to an internal package
- `DefaultClientRequestContext`, `DefaultServiceRequestContext`, `NonWrappingRequestContext` have been moved to an internal package
    - `pathWithQuery` is moved to `ClientUtil` and made public
    - `ClientThreadLocalState` is moved to an internal package and made public
- Class hierarchy has been modified for `RequestContext`
    - Now there is an extra layer between `ClientRequestContext` and `DefaultClientRequestContext`:
        - `ClientRequestContext` -> `ClientRequestContextExtension` -> `DefaultClientRequestContext`
    - Now there is an extra layer between `RequestContext` and `NonWrappingRequestContext`
        - `RequestContext` -> `RequestContextExtension` -> `NonWrappingRequestContext`

Before:
<img width="300" alt="Screen Shot 2022-06-24 at 12 00 49 PM" src="https://user-images.githubusercontent.com/8510579/175453672-15dbdf09-4c2e-4afa-a186-a2147e97afa2.png">
After:
<img width="300" alt="Screen Shot 2022-06-24 at 12 01 15 PM" src="https://user-images.githubusercontent.com/8510579/175453689-3c00edde-7209-4d1c-9480-e776c58537e0.png">

Result:

- Developers may add more utility functions to `RequestContext` without worrying about public API exposure
- Armeria tries to unwrap `RequestContext` instead of simply checking the type.
minwoox pushed a commit that referenced this pull request Oct 18, 2022
Motivation:

RequestContextWrapper$delegate is deprecated since #4308 

Modifications:

- replaced all occurrences of delegate() with unwrap()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants