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

Support AsyncFile, Path. Added PathPart and FilePart #16286

Merged
merged 2 commits into from Apr 19, 2021

Conversation

FroMage
Copy link
Member

@FroMage FroMage commented Apr 6, 2021

Support for new file types and partial versions.

I need guidance for how to handle AsyncFile properly though in the case that there are writer interceptors, because our current infra doesn't support suspending in the writer if there are writer interceptors, and if we're in the IO thread I can't very well block in the writer to read the file.

We could detect a return type of AsyncFile at build time, and throw if there are writer interceptors and force the user to use @Blocking on the endpoint.

Or we start supporting @Blocking on the writer, but that sounds like a lot of work.

Any advice @stuartwdouglas ?

@geoand
Copy link
Contributor

geoand commented Apr 6, 2021

We could detect a return type of AsyncFile at build time, and throw if there are writer interceptors and force the user to use @Blocking on the endpoint.

I think this is a reasonable way to start out

public void handle(Void event) {
response.end();
// Not sure if I need to resume, actually
ctx.resume();
Copy link
Contributor

Choose a reason for hiding this comment

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

Likely not, since there should be no more handlers in the chain, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, there's always cleanup, no?

Copy link
Contributor

Choose a reason for hiding this comment

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

What cleanup? I mean if this is the last Handler in the chain, resuming should do nothing but push the context into the event loop and just return when the event actually gets processed

Copy link
Member Author

Choose a reason for hiding this comment

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

In the context.close we do things like call async listeners and tear down the CDI scope and stuff, no?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, good point

ctx.suspend();
ServerHttpResponse response = context.serverResponse();
response.setChunked(true);
file.handler(buffer -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we also need an exception handler?

Copy link
Member Author

Choose a reason for hiding this comment

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

Very good point.

import javax.ws.rs.ext.MessageBodyWriter;
import org.jboss.resteasy.reactive.FilePart;

public class FilePartBodyHandler implements MessageBodyWriter<FilePart> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't it be best to suffix these with BodyWriter instead of BodyHandler which is rather overloaded?

Copy link
Member Author

Choose a reason for hiding this comment

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

I copied the other class names, but yeah, in this case they're write-only, so good point.


@Provider
@Produces("*/*")
@Consumes("*/*")
Copy link
Contributor

Choose a reason for hiding this comment

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

Consumes shouldn't be needed, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Haha, yes!

@geoand
Copy link
Contributor

geoand commented Apr 7, 2021

Also, do we really need both FilePart and PathPart? I would propose we only use the latter

@FroMage
Copy link
Member Author

FroMage commented Apr 7, 2021

Also, do we really need both FilePart and PathPart? I would propose we only use the latter

I sorta agree, but given that we support both File and Path, I think it's friendlier to provide both.

@geoand
Copy link
Contributor

geoand commented Apr 7, 2021

Also, do we really need both FilePart and PathPart? I would propose we only use the latter

I sorta agree, but given that we support both File and Path, I think it's friendlier to provide both.

Yeah, I don't mind either way

@FroMage
Copy link
Member Author

FroMage commented Apr 12, 2021

We could detect a return type of AsyncFile at build time, and throw if there are writer interceptors and force the user to use @Blocking on the endpoint.

So, I thought about this a bit more, and it turns out that I don't think this type can work with writer interceptors at all, in fact.

WriterInterceptor depends on the writer using blocking IO and using OutputStream to write, which is blocking, so all the writing must be done from the worker thread.

AsyncFile does all its reading from the IO thread.

  • If we're on the IO thread, I can't very well use blocking IO, because it would block itself.
  • If we're on a worker thread, I could do all the reading from the IO thread and the writing on the worker thread, but performance is going to be horrible, so I don't see the point in supporting that.

I'm just going to disallow AsyncFile having any WriterInterceptor.

But also, I'm now wondering if our architecture really works if we have any WriterInterceptor and we're on the IO thread? Because that will force blocking IO, and potentially block the IO thread.

WDYT @stuartwdouglas @geoand ?

@FroMage
Copy link
Member Author

FroMage commented Apr 12, 2021

Also, I fucking hate IO and in particular blocking and async IO. BOTH.

@geoand
Copy link
Contributor

geoand commented Apr 12, 2021

We could detect a return type of AsyncFile at build time, and throw if there are writer interceptors and force the user to use @Blocking on the endpoint.

So, I thought about this a bit more, and it turns out that I don't think this type can work with writer interceptors at all, in fact.

WriterInterceptor depends on the writer using blocking IO and using OutputStream to write, which is blocking, so all the writing must be done from the worker thread.

AsyncFile does all its reading from the IO thread.

  • If we're on the IO thread, I can't very well use blocking IO, because it would block itself.
  • If we're on a worker thread, I could do all the reading from the IO thread and the writing on the worker thread, but performance is going to be horrible, so I don't see the point in supporting that.

I'm just going to disallow AsyncFile having any WriterInterceptor.

But also, I'm now wondering if our architecture really works if we have any WriterInterceptor and we're on the IO thread? Because that will force blocking IO, and potentially block the IO thread.

+1

WDYT @stuartwdouglas @geoand ?

Yeah, I've wondered about this myself at times.

@stuartwdouglas
Copy link
Member

Yea, this is complex, the blocking API's that JAX-RS provides don't work well with non-blocking.

At the moment our output stream will just fully buffer if you are on the IO thread, which works fine for small responses but is not going to work for large responses such as files.

Maybe switch to the worker thread whenever WriterInterceptors are involved, but introduce our own non-blocking version?

In general though I think for safety maybe this whole block needs to be run in a worker thread:

if (writer.isWriteable(entity.getClass(), context.getGenericReturnType(), context.getAllAnnotations(),

}

@Override
public boolean isWriteQueueFull() {
Copy link
Member

Choose a reason for hiding this comment

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

isWriteQueueFull and addDrainHandler don't really make sense here. The interface really needs some Javadoc but at the moment write() is assuming you can write once, then you need to wait for a notification.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, I honestly didn't know how to implement those. Any hint? I did need them for the vertx async part.


@Override
public ServerHttpResponse sendFile(String path, long offset, long length) {
context.response().sendFile(path, offset, length);
Copy link
Member

Choose a reason for hiding this comment

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

This bypasses a lot of Servlet stuff, it really needs to write the file using the Servlet API's.

Copy link
Member Author

Choose a reason for hiding this comment

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

Are there any specific APIs for servlet to write files?

@FroMage
Copy link
Member Author

FroMage commented Apr 13, 2021

Maybe switch to the worker thread whenever WriterInterceptors are involved, but introduce our own non-blocking version?

Well, yeah, perhaps. I had to do that for Async-IO in RESTEasy Classic. I was hoping not to have to do it again, but…

@FroMage FroMage force-pushed the 15891 branch 2 times, most recently from 3c6c576 to 6386fea Compare April 13, 2021 15:32
@FroMage FroMage marked this pull request as ready for review April 13, 2021 15:35
@quarkus-bot
Copy link

quarkus-bot bot commented Apr 13, 2021

This workflow status is outdated as a new workflow run has been triggered.

Failing Jobs - Building 6386fea

Status Name Step Test failures Logs Raw logs
JVM Tests - JDK 11 Build Test failures Logs Raw logs
JVM Tests - JDK 11 Windows Build Test failures Logs Raw logs
JVM Tests - JDK 15 Build Test failures Logs Raw logs
JVM Tests - JDK 8 Build ⚠️ Check → Logs Raw logs
MicroProfile TCKs Tests Run RESTEasy Reactive TCK Test failures Logs Raw logs

Full information is available in the Build summary check run.

Test Failures

⚙️ JVM Tests - JDK 11 #

📦 integration-tests/kafka

io.quarkus.it.kafka.SaslKafkaConsumerTest.testReception line 48 - More details - Source on GitHub

io.quarkus.it.kafka.SslKafkaConsumerTest.testReception line 56 - More details - Source on GitHub


⚙️ JVM Tests - JDK 11 Windows #

📦 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment

io.quarkus.resteasy.reactive.server.test.providers.FileTestCase.testFiles line 51 - More details - Source on GitHub


⚙️ JVM Tests - JDK 15 #

📦 integration-tests/kafka

io.quarkus.it.kafka.SaslKafkaConsumerTest.testReception line 48 - More details - Source on GitHub

io.quarkus.it.kafka.SslKafkaConsumerTest.testReception line 56 - More details - Source on GitHub


⚙️ MicroProfile TCKs Tests #

📦 resteasy-reactive-testsuite/tests

com.sun.ts.tests.jaxrs.spec.filter.interceptor.JAXRSClient0021.fileWriterClientInterceptorTest line 591 - More details - Source on GitHub

@FroMage
Copy link
Member Author

FroMage commented Apr 14, 2021

OK, the kafka failures are not mine. But this Windows failure is very interesting:

Expected header "Content-Length" was not "448", was "453". Headers are:
content-length=453

So the file size is different on windows? That's… how do we explain that?

@geoand
Copy link
Contributor

geoand commented Apr 14, 2021

OK, the kafka failures are not mine.

If you rebase onto the current main, then those will likely go away

@geoand
Copy link
Contributor

geoand commented Apr 14, 2021

So the file size is different on windows? That's… how do we explain that?

This is probably stupid, but line endings maybe?

@FroMage
Copy link
Member Author

FroMage commented Apr 14, 2021

This is probably stupid, but line endings maybe?

Well, but who would remove/add them?

@FroMage
Copy link
Member Author

FroMage commented Apr 14, 2021

The TCK failure is also interesting:

It's a WriterInterceptor that hijacks the OutputStream where we write the File, and replaces the contents with other stuff. Meanwhile I added the Content-Length header in the File writer, because we know it and it's good form. But it doesn't work if the output isn't actually what we produce.

This looks like a silly test: it should probably remove the Content-Length header, no?

Or should I drop the Content-Length when returning a File? That seems a bit wrong, no? TCK or not, I think it's the interceptor's fault for making a mess and it should clean up, no?

@geoand
Copy link
Contributor

geoand commented Apr 14, 2021

This is probably stupid, but line endings maybe?

Well, but who would remove/add them?

Well, the server reads the file from the filesystem, right? So in that case it would likely add the Windows line ending when sending the file back, while the test is looking for the string length having the string memory, no?

@geoand
Copy link
Contributor

geoand commented Apr 14, 2021

The TCK failure is also interesting:

It's a WriterInterceptor that hijacks the OutputStream where we write the File, and replaces the contents with other stuff. Meanwhile I added the Content-Length header in the File writer, because we know it and it's good form. But it doesn't work if the output isn't actually what we produce.

This looks like a silly test: it should probably remove the Content-Length header, no?

Yeah, I would expect that.

Or should I drop the Content-Length when returning a File? That seems a bit wrong, no? TCK or not, I think it's the interceptor's fault for making a mess and it should clean up, no?

I think you have done the right thing and the test is making a mess

@geoand
Copy link
Contributor

geoand commented Apr 14, 2021

This is probably stupid, but line endings maybe?

Well, but who would remove/add them?

Well, the server reads the file from the filesystem, right? So in that case it would likely add the Windows line ending when sending the file back, while the test is looking for the string length having the string memory, no?

Also, are there 5 lines in the result? If not, we can simply ignore my comment :)

@FroMage
Copy link
Member Author

FroMage commented Apr 14, 2021

Also, are there 5 lines in the result? If not, we can simply ignore my comment :)

There are. But I don't get it. I'm expecting a file of size 448, which is the size of the String I'm comparing it to.
The server reads it as bytes, it should never interpret any line endings while reading or writing. I'm not even looking at encoding or anything. And I get back from the server 5 bytes too many. Because it set that header, so the file system must have reported it as 5 bytes more.

This isn't even about encoding or something adding line endings. I don't think the FS on Windows goes and reports file sizes with adjusted line endings.
Unless, the checked-out lorem.txt file itself has been adjusted. Huh, I guess that's possible. Perhaps git does that, or some other process we have in place.

FroMage added a commit to quarkusio/resteasy-reactive-testsuite that referenced this pull request Apr 14, 2021
Because it changes the content, and the server is right in adding this header.
See quarkusio/quarkus#16286
@FroMage
Copy link
Member Author

FroMage commented Apr 14, 2021

OK, adjusted for Windows, pushed new TCK, rebased on main.

@quarkus-bot
Copy link

quarkus-bot bot commented Apr 14, 2021

Failing Jobs - Building 8151c96

Status Name Step Test failures Logs Raw logs
Gradle Tests - JDK 11 Windows Verify dependencies Test failures Logs Raw logs

Full information is available in the Build summary check run.

Test Failures

⚙️ Gradle Tests - JDK 11 Windows #

📦 integration-tests/gradle

io.quarkus.gradle.devmode.QuarkusDevDependencyDevModeTest.main() line 14 - More details - Source on GitHub

@FroMage
Copy link
Member Author

FroMage commented Apr 14, 2021

I don't see how that gradle/windows/devmode failure can be related, so restarting.

@geoand
Copy link
Contributor

geoand commented Apr 14, 2021

It's probably just a flake

@FroMage
Copy link
Member Author

FroMage commented Apr 14, 2021

CI actually passed, but the Build summary failed. What do we do in these cases @gsmet ?

@gsmet
Copy link
Member

gsmet commented Apr 19, 2021

Ah I know, the outdated status was probably not hidden properly. I'll take this case into account. You can safely merge this if all is good for you all.

@geoand
Copy link
Contributor

geoand commented Apr 19, 2021

@FroMage I assume this is ready to go?

@cescoffier cescoffier merged commit 086c700 into quarkusio:main Apr 19, 2021
@quarkus-bot quarkus-bot bot added this to the 2.0 - main milestone Apr 19, 2021
@cescoffier
Copy link
Member

Merged!

@cescoffier
Copy link
Member

(@FroMage told me it was ok)

@geoand
Copy link
Contributor

geoand commented Apr 19, 2021

👍🏼

@FroMage
Copy link
Member Author

FroMage commented Apr 19, 2021

Famous Last Words™

Thanks ;)

@FroMage FroMage linked an issue Apr 20, 2021 that may be closed by this pull request
FroMage added a commit to quarkusio/resteasy-reactive-testsuite that referenced this pull request Apr 20, 2021
Because it changes the content, and the server is right in adding this header.
See quarkusio/quarkus#16286
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.

Add streaming support for files in RESTEasy Reactive
5 participants