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

WIP: Test gzip support in RESTEasy Reactive #16924

Closed
wants to merge 9 commits into from

Conversation

saumya1singh
Copy link
Contributor

CC @FroMage :)

Copy link
Contributor

@netodevel netodevel left a comment

Choose a reason for hiding this comment

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

@SaumyaSingh1,
I suggested some small changes.

.addClasses(BeanRegisteringRouteUsingObserves.class));

@Test
public void test() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

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

method nomenclature is too generic.
If you can make it clear what is being tested

public class GZipTest {

private static final String APP_PROPS = "" +
"quarkus.http.enable-compression=true\n";
Copy link
Contributor

@netodevel netodevel Apr 30, 2021

Choose a reason for hiding this comment

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

what do you think of a test with the other way?

quarkus.http.enable-compression=false

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If it is disabled, should we really perform any sort of test? I don't think that's needed when is not even enabled :)

Copy link
Contributor

@netodevel netodevel May 1, 2021

Choose a reason for hiding this comment

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

but how to ensure that the property is working correctly?
and how do you make sure that no future code from other developers doesn't affect this and go undetected because not have any tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@FroMage WDYT

Copy link
Member

Choose a reason for hiding this comment

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

Well, we should test the default behaviour, that's true, but because we know what the default is ATM, we want to test the default behaviour without any configuration, to make sure it won't be compressed by default.


public void register(@Observes Router router) {

router.route("/compression").handler(rc -> {
Copy link
Member

Choose a reason for hiding this comment

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

This is testing reactive routes, but we want to be testing RESTEasy Reactive :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From available articles (https://quarkus.io/guides/reactive-routes) I got the reactive route part, I tried searching for resources around testing RR but didn't get. Maybe we are supposed to do something like ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh wait, perhaps I should be testing this against some RR endpoint

Copy link
Member

Choose a reason for hiding this comment

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

Yes, indeed.

.header("content-length", Matchers.not(Matchers.equalTo(Integer.toString(longString.length()))))
.body(Matchers.equalTo(longString));

RestAssured.given().get("/nocompression").then().statusCode(200)
Copy link
Member

Choose a reason for hiding this comment

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

For good measure, we should probably also test that we can send compressed data to the server.

Copy link
Contributor Author

@saumya1singh saumya1singh May 10, 2021

Choose a reason for hiding this comment

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

So here we want to test if the data we are sending is compressed or not ( i.e Inputstream is gzipped or zipped ). Right?

Or do we want to compress the files first before sending them? (but there will be no purpose of enabling gzip then, so not this one)

saumya1singh and others added 3 commits May 6, 2021 12:11
…ent/src/test/java/io/quarkus/resteasy/reactive/GZipTest.java

Co-authored-by: Stéphane Épardaud <[email protected]>
Registering endpoints
@saumya1singh saumya1singh requested a review from FroMage May 7, 2021 02:26
@saumya1singh
Copy link
Contributor Author

saumya1singh commented May 10, 2021

@FroMage
#16425 (comment)

In order to automate the process of "stoping the compression" we want to create an annotation.

Situations/conditions when we want to stop/disable compression -

  1. If the data/files are already compressed (can be checked by magic number concept, .zip or .gz begins with specific magic num. OR by extension type?)

  2. If there are images (.png or .gif ) then we want to stop compression

.header("content-encoding", "identity")
.header("content-length", Matchers.equalTo(Integer.toString(longString.length())))
.body(Matchers.equalTo(longString));
}
Copy link
Member

Choose a reason for hiding this comment

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

So does this test pass? I don't see anything in the endpoints that would justify a different behaviour.
Could you test with returning a binary reponse, such as an image? Just to see what vert.x does in this case, if it stll tries to compress it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

image
It is failing with this info.
For endpoint /test/nocompression

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I created an image and returned to the endpoint. This time also, content-encoding is gzip.
image

Copy link
Contributor Author

@saumya1singh saumya1singh May 10, 2021

Choose a reason for hiding this comment

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

So in the beginning I am initializing APP_PROPS as true

private static final String APP_PROPS = "" +
            "quarkus.http.enable-compression=true\n";

perhaps that's why content encoding is always gzip even for Binary Data i.e Images

That means vert.x is trying to compress the binary data as well, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When I set quarkus.http.enable-compression to false , the content-length=24000 bytes which is far more than 130 bytes (for text) or 165 bytes(for image)

private static final String APP_PROPS = "" +
            "quarkus.http.enable-compression=false\n";

image

Copy link
Contributor Author

@saumya1singh saumya1singh May 10, 2021

Choose a reason for hiding this comment

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

So perhaps the conclusion is - It is trying to compress images as well.

Am I right @FroMage?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, probably, could you add that test to your PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah sure, I missed this comment.

@FroMage
Copy link
Member

FroMage commented May 10, 2021

Situations/conditions when we want to stop/disable compression -
If the data/files are already compressed (can be checked by magic number concept, .zip or .gz begins with specific magic num. OR by extension type?)
If there are images (.png or .gif ) then we want to stop compression

Well, first I'd like to see you test what the current behaviour is: will binary types get compressed or not?

@saumya1singh
Copy link
Contributor Author

Well, first I'd like to see you test what the current behaviour is: will binary types get compressed or not?

Seems yes.

For images :

  1. When quarkus.http.enable-compression is false ->

image

  1. When quarkus.http.enable-compression is true ->

image

@FroMage
Copy link
Member

FroMage commented May 10, 2021

Since we can't enable compression on a per-endpoint basis, I guess we really need to add a @DisableCompression annotation, and make RESTEasy Reactive automatically add the header as an extra handler.

@saumya1singh
Copy link
Contributor Author

Interesting, so now I am supposed to create an annotation @DisableCompression.
This annotation should automatically add the header Transfer-Encoding: identity to stop compression.

And we will add this annotation at the endpoint test/nocompression.

@FroMage
Copy link
Member

FroMage commented May 10, 2021

Yes.


RestAssured.given().get("/test/nocompression").then().statusCode(200)
.header("content-encoding", "identity")
.header("content-length", Matchers.equalTo((long) (createImage().getData().getDataBuffer().getSize() * 4L)))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(long) (createImage().getData().getDataBuffer().getSize() * 4L)

Finding the content-length for BufferedImage is something I am not much sure about.
I took reference from here -
https://stackoverflow.com/questions/632229/how-to-calculate-java-bufferedimage-filesize?noredirect=1&lq=1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The size of this png image(original) is 2.91kb OR 2910 bytes

Using the above technique, the size is 360000 bytes.

On calculating content-length using ByteArrayOutputStream I am getting 2914 bytes. ✔️
So perhaps this is the correct techniques to get the byte size of BufferedImage :

            BufferedImage image = createImage();
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            ImageIO.write(image, "png", outputStream);
            outputStream.close();
            long contentLength = outputStream.size();
            System.out.println(" content-length using ByteArrayOutputStream => " + contentLength );


image

Copy link
Member

Choose a reason for hiding this comment

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

Actually you can just use a static test image as a resource :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Humm I thought it's better to generate an image only when this test is run.

You are right, it will make code simpler. Instead of creating an image through code it is better to use a static image 💯

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Original size - 2914 bytes
Compressed size - 2741 bytes
image

ImageIO.write(bufferedImage, "png", file);
return bufferedImage;
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The image I am creating in createImage() looks like this -

image

Perhaps "saving the image" is optional.

@saumya1singh
Copy link
Contributor Author

saumya1singh commented May 11, 2021

I have created the annotations, but I am yet to understand and implement the logic that how to make my custom annotation set the header Transfer-Encoding to identity.

Am I going in the right direction? @FroMage

@FroMage
Copy link
Member

FroMage commented May 11, 2021

I think your annotation should live next to ServerExceptionMapper, and then you have to make EndpointIndexer.createResourceMethod understand that annotation, and pass the info that compression is disabled in ResourceMethod which is used by RuntimeResourceDeployment.buildResourceMethod in order to create the set of ServerRestHandler that are to be called for the endpoint.

So if the endpoint has compression disabled, we need a new handler that adds the required header after the endpoint is invoked, probably before this line:

        addHandlers(handlers, method, info, HandlerChainCustomizer.Phase.AFTER_METHOD_INVOKE);

BTW, I'll be on PTO the next three days so take your time, or ask others for help on Zulip if you need to, but it's possible they'll also be on PTO.

@saumya1singh
Copy link
Contributor Author

Sure, it's public holiday here as well (tomorrow ig).

I will use your explanation ^ and try to understand, implement it.

@saumya1singh saumya1singh changed the title WIP: gzip support in RESTEasy Reactive WIP: Test gzip support in RESTEasy Reactive May 13, 2021
@saumya1singh
Copy link
Contributor Author

and then you have to make EndpointIndexer.createResourceMethod understand that annotation, and pass the info that compression is disabled in ResourceMethod

In order to do this ^ I tried thinking of these 2 ways :

1. Should I directly introduce the new annotation inside createResourceMethod() in EndPointIndexer.java

private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo,
and pass the info that compression is disabled somewhere in a new ResourceMethod
ResourceMethod method = createResourceMethod(currentMethodInfo, actualEndpointInfo, methodContext)

OR
2. In QuarkusServerEndpointIndexer

override createResourceMethod() and set the properties in a way to disable compression.

@saumya1singh
Copy link
Contributor Author

While working on the previous filter and other tasks I was easily able to debug my code written in DevConsoleProcessor or other files by hitting the DEv-UI endpoints such as http://localhost:8080/q/dev/io.quarkus.quarkus-resteasy-reactive/endpoints.

How should I go about it here e.g to check breakpoints in EndpointIndexer
https://github.com/quarkusio/quarkus/blob/c88b990f3163785e0eaaa59acd98db4c8657793c/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java

@FroMage
Copy link
Member

FroMage commented May 18, 2021

I would say option 1, because this is not specific to Quarkus.

@FroMage
Copy link
Member

FroMage commented May 18, 2021

Breaking on the indexer requires you to run the build steps, but you can do that by adding a QuarkusUnitTest in quarkus-resteasy-reactive-deployment like the others there, and running that test will invoke the build steps and therefore your breakpoint.

@gastaldi
Copy link
Contributor

Hi @saumya1singh, any chance you'll resume this soon?

@saumya1singh
Copy link
Contributor Author

hey @gastaldi, after 2.2.5 I am focusing on the Elektra release (plus have to provide training sessions to new interns in prod) , so can't give much time on this issue :/

@gastaldi
Copy link
Contributor

@saumya1singh no worries, just curious 😉

@mkouba
Copy link
Contributor

mkouba commented Mar 15, 2022

For the record - it seems that vertx will try to compress the static resources as well, see the StaticHandler.skipCompressionForMediaTypes() and StaticHandler.skipCompressionForSuffixes() methods. And I think that we should make this configurable too.

@mkouba
Copy link
Contributor

mkouba commented Mar 15, 2022

Hm, to be honest I don't like the idea of opt-out via @DisableCompression (and config properties for static resources). I do understand that we're limited by how vertx-web implements compression but still.. it's not very flexible.

In theory, we could introduce two annotations (@Compressed and @Uncompressed) and a property to specify the default behavior for endpoints if quarkus.http.enable-compression=true, e.g. something like quarkus.resteasy-reactive.endpoints-compressed (false by default?). Furthermore, we could introduce a simple @ServerResponseFilter that would add the Transfer-Encoding: identity header if the resource method is annotated with @Uncompressed OR quarkus.resteasy-reactive.endpoints-compressed=false and the method is not annotated with @Compressed. @saumya1singh @FroMage WDYT?

@gsmet
Copy link
Member

gsmet commented Mar 21, 2022

@geoand @cescoffier could we try to come up with a plan so that @mkouba can put it into motion.

As for the requirements, I think:

  • we should be able to compress endpoints without compressing static resources (and reciprocally)
  • we should be able to enable/disable compression (with a global flag and then opt-in/opt-out)
  • we will need to make sure gzip works for Reactive REST Client - I'm not sure if we handle GZip there at the moment

I don't think we need fancy algorithm to start with. Let's keep it simple and we will see later if we can expand.

@geoand
Copy link
Contributor

geoand commented Mar 21, 2022

I'll have a look (although I know nothing about how Vert.x supports gzip), but honestly I don't conside this terribly important fot 2.8.

@cescoffier
Copy link
Member

Sorry, I don't have much availability this week.

First, the static handler can skip compression for a set of media types or extensions. I think we should rely on this.

Basically, I would recommend enabling compression by default and letting decide compression vs. raw using the annotation proposed by @mkouba. I know it's a big change in terms of behavior, but I think it's reasonable, and the user can go back to the previous behavior by disabling the compression globally.

So, in other words, I agree with @mkouba's plan. We would need to document how to skip compression for static files based on the media types (I'm not sure we expose these settings, so we should).

Note that compression is handled at the netty level. Basically, we add the netty handler compressing the response. That's why it's global.

@gsmet
Copy link
Member

gsmet commented Mar 21, 2022

honestly I don't consider this terribly important fot 2.8

I tend to disagree with that. We had numerous reports/questions about usage of the GZip option and it's especially important if you're paying your bandwidth bills in a cloud environment.

@cescoffier
Copy link
Member

It's a question of trade off as you use more cpu and so cannot handle the same level of concurrency (which means you may need more instances).

I also wanted to add the vertx 4.3 is going to extend how is handled compression. That may give us more flexibility, however that's for June (and I do not believe we can wait)

@mkouba
Copy link
Contributor

mkouba commented Mar 22, 2022

Ok, to sum it up:

  • we should change the default so that quarkus.http.enable-compression=true
  • introduce @Compressed and @Uncompressed
  • introduce a config property to skip compression for static resources; and maybe we should skip ALL by default (the only problem is that StaticHandler.skipCompressionForMediaTypes() and StaticHandler.skipCompressionForSuffixes() methods accept an explicit set and don't support wildcards or anything like that)
  • (?) introduce a config property to enable/disable compression for a specific technology, e.g. resteasy-reactive resource methods via quarkus.resteasy-reactive.compressed-endpoints, false by default

This would allow a user to:

  • by default: just add @Compressed to a specific resource method and other resource methods would remain uncompressed
  • enable compression for all resource methods via quarkus.resteasy-reactive.compressed-endpoints=true and then opt-out via @Uncompressed where needed
  • enable compression for some static resources
  • disable the compression altogether with quarkus.http.enable-compression=false

@geoand
Copy link
Contributor

geoand commented Mar 22, 2022

I tend to disagree with that. We had numerous reports/questions about usage of the GZip option and it's especially important if you're paying your bandwidth bills in a cloud environment.

Not that I have seen

@geoand
Copy link
Contributor

geoand commented Mar 22, 2022

This would allow a user to:

  • by default: just add @Compressed to a specific resource method and other resource methods would remain uncompressed
  • enable compression for all resource methods via quarkus.resteasy-reactive.compressed-endpoints=true and then opt-out via @Uncompressed where needed
  • enable compression for some static resources
  • disable the compression altogether with quarkus.http.enable-compression=false

Seems reasonable in theory, but I don't really understand how compression can be used on per resource basis.

@mkouba
Copy link
Contributor

mkouba commented Mar 22, 2022

Seems reasonable in theory, but I don't really understand how compression can be used on per resource basis.

Well, I can imagine a resource with methods that return large JSON payloads and also with a method foo() that returns a zip file. In this case, you'd want to quarkus.resteasy-reactive.compressed-endpoints=true and foo() annotated with @Uncompressed?

@geoand
Copy link
Contributor

geoand commented Mar 22, 2022

My question was mainly from an implementation point of view. So basically what you are proposing is that we have two different ways of compressing output, one general one based on Vert.x and one custom one?

@mkouba
Copy link
Contributor

mkouba commented Mar 22, 2022

My question was mainly from an implementation point of view. So basically what you are proposing is that we have two different ways of compressing output, one general one based on Vert.x and one custom one?

Yes, but maybe I'm just overthinking on this issue ;-).

The thing is that if we merge the quarkus.http.enable-compression and quarkus.resteasy-reactive.compressed-endpoints; and interpret quarkus.http.enable-compression=true as "compress all endpoints in all technologies" (resteasy-reactive, reactive-routes) then you won't be able to disable compression for endpoints by default an opt-in via @Compressed (i.e. there's no need for this annotation). Which might be acceptable. I'm not sure.

@FroMage
Copy link
Member

FroMage commented Mar 22, 2022

It could be useful to look at how web servers do it: https://httpd.apache.org/docs/2.4/mod/mod_deflate.html and https://docs.nginx.com/nginx/admin-guide/web-server/compression/

Apparently, Apache doesn't enable anything by default, but gives two documented options:

  • Compress html, text, js, css
  • Compress everything but images

Nginx by default will compress only html, but documents the option to compress also xml

From that, I gather that we could go the same way and provide an option for compression by mime types:

quarkus.http.enable-compression=text/html,text/plain,text/xml,text/css,text/javascript,application/javascript

I guess that would accept MIME wildcards too. This applies to static resources as well as endpoints. This seems to be what makes the most sense and is the most flexible, no?
Or, we make it two options:

quarkus.http.enable-compression=true
# this is the default value, when compression is enabled
#quarkus.http.enable-compression.for=text/html,text/plain,text/xml,text/css,text/javascript,application/javascript

Now, sure, we can also support an annotation to make it per endpoint, but IMO that's a "nice to have" and not as important as this sort of config.

@geoand
Copy link
Contributor

geoand commented Mar 22, 2022

My question was mainly from an implementation point of view. So basically what you are proposing is that we have two different ways of compressing output, one general one based on Vert.x and one custom one?

Yes, but maybe I'm just overthinking on this issue ;-).

The thing is that if we merge the quarkus.http.enable-compression and quarkus.resteasy-reactive.compressed-endpoints; and interpret quarkus.http.enable-compression=true as "compress all endpoints in all technologies" (resteasy-reactive, reactive-routes) then you won't be able to disable compression for endpoints by default an opt-in via @Compressed (i.e. there's no need for this annotation). Which might be acceptable. I'm not sure.

Yeah, this is exactly why I think we should not rush into this... It seems to me like we need to put some more thought into it.

@mkouba
Copy link
Contributor

mkouba commented Mar 22, 2022

From that, I gather that we could go the same way and provide an option for compression by mime types:

Well, I like the idea but the problem is that StaticHandler currently does not support the opt-in approach; i.e. you can only say "enable compression and disable for a set of media types".

I guess that would accept MIME wildcards too.

I don't think the current StaticHandlerImpl supports this; it seems to use Object.equals().

@FroMage @geoand

@mkouba
Copy link
Contributor

mkouba commented Mar 24, 2022

From that, I gather that we could go the same way and provide an option for compression by mime types:

Well, I like the idea but the problem is that StaticHandler currently does not support the opt-in approach; i.e. you can only say "enable compression and disable for a set of media types".

@cescoffier had an idea that we could add Content-Encoding: identity to every response automatically (if quarkus.http.enable-compression=true) and let the next route in the chain decide whether to remove this header (and enable compression). I've experimented with this approach and it seems to work. I will add a link to my WIP branch once the GitHub UI is in sync with my quarkus repository...

@mkouba
Copy link
Contributor

mkouba commented Mar 24, 2022

@geoand
Copy link
Contributor

geoand commented Oct 18, 2022

Closing as this has been addressed in #24558

@geoand geoand closed this Oct 18, 2022
@quarkus-bot quarkus-bot bot added the triage/invalid This doesn't seem right label Oct 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/rest triage/invalid This doesn't seem right
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants