-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Experiment with a fully async ContentSourceCompletableFuture #9975
Experiment with a fully async ContentSourceCompletableFuture #9975
Conversation
For use by MultiPartFormData and MultiPartByteRanges
...re/jetty-client/src/test/java/org/eclipse/jetty/client/util/MultiPartRequestContentTest.java
Outdated
Show resolved
Hide resolved
@gregw sorry but
Not sure what you mean, as all I'm pretty much -1 on this PR, I don't see the savings, the two classes used in different contexts, and while there is a little duplication, I don't think it's worth an abstract class, also considering that the current duplication may not be a duplication anymore if other parameters are added -- for example max parse length, etc. I also don't see your point about double parsing? |
@sbordet please take time to understand before you -1. Currently in too many places we are taking back to blocking... Which is so stupid after making so the effort to make our async so much better. By all means give feed back, but hold the veto until I've been able to answer your questions. |
What I find weird is that it contains code to efficiently/asynchronously consume input from a
That's what
Nope, it is the otherway around. It is confusing that we have code that is directly managing chunks to be parsed. We already have an abstraction for a sequences of
See I missed that because it was using the class in a strange way. It was not calling I'm not saying that we get rid of the class. I'm saying that we factor out the duplicate code with
The code that is reading an InputStream, converting it to Chunks and then calling
Hey - as I said before. No veto until you've understood and given me a chance to answer your questions. Besides, it is an experiment tagged for 12.0.1, so no need to decide either way now.
The IO code is duplicated in both classes, and then not used in several places where more IO code is written, duplicating either the same IO code again or other utility IO code that we have. Perhaps the IO code should not be in either class? but either way, we have far too much duplication of very similar code that is absolutely crucial to both performance and correctness to get right. Having that code in well maintained utility or base classes is a good thing to do. Managing the whole conversation between turning a
OK I missed that |
Agreed on single-use of I'm not opposed to having Agreed that
It's not using it in a strange way 😃 About the veto, I think I'm using -1 too liberally as in "I don't agree with this PR, but open to discussion" -- I'll use -0 in future.
I disagree. if (content-type is multipart/form-data) {
try lookup attribute
else explicit parsing
} else {
// some other request type
} If this style is confirmed, then we either have a On the client, we don't need the attribute as nothing parses multipart if not the application itself. Accessing the Servlet multipart config from a delayed handler I think would be hard... not a big fan of delaying TBH (I prefer lazy for this case). |
There is something a bit strange, or at least different in how we have a class that is a container, a parser and a generator. We might needs some restructuring here, or perhaps just some renaming? or maybe a bit of both. However, that might be for another PR. I started this PR because of the duplication I saw in the IO patterns. Thus my main goal is to generalise the IO that is done around these types of classes. Once that is done and agreed, I think we can look to see if this PR should also include some restructuring/renaming or if that is separate.
Or is the CompletableFuture abstraction something separate that just happens to use the parser to provide a container in the future?
OK - bad choice of words. But see above, it can be a surprise to see a class that looks like it is primarily a parser, but then discover it is primarily used as a generator. Elsewhere we are very explicit with the Parser/Generator naming. We have a different pattern here, without clarity or consistency. The pattern might be fine, but we need better consistency to help with clarity.
Definitely too liberal with the -1's. I also think that a draft PR of an experimental idea targeted for 2 or 3 releases in the future also doesn't need a -0. This PR or anything that comes from it is weeks if not months away from needing any ± decisions. Questions, dislikes, clarifications are all worthwhile at this stage, but a -0 helps little with a draft of an experiment.
Yes and no. Anything that moves to servlet land immediately needs at least 2 versions of it, so trying to keep things core is worthwhile. But then the config class itself it a servlet API class. So we need an simple/elegant way for a core handler to suck servlet specific configuration out of a context in a typeless way. Currently no good ideas for this, so 2 API dependent versions might be the best.
Lazy is not good enough. Once we get to servlet-land, then we are naturally blocking. We can go back to async, but not without dispatch type changes that makes anything we do less transparent. I'm not a big fan of the DelayedHandler in its current form, as I think it is trying to do too much for too many different situations. I think we should just give up on the whole delay dispatch until the first data optimisation.. or at last just have a handler dedicated to just that. What this PR is about is ensuring that we can delay dispatch until the entire content is asynchronously consumed (and written to temporary files etc.) before invoking the servlet. Perhaps we should call this "pre-parsing" or "pre-loading" rather than "delaying"? Pre-loading is best done in a core handler before hitting the But there is no hurry on this stuff, it is all internal and can be done after 12.0.0. I only worked on it yesterday and today as I had 4 hours on a train and didn't want to work on anything more critical. |
Restructure MultiPartFormData to have a Parser class
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.
Looks good.
Needs MultiPartByteRanges
to be updated in the same style.
...core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceCompletableFuture.java
Show resolved
Hide resolved
jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java
Outdated
Show resolved
Hide resolved
jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/AsyncContent.java
Outdated
Show resolved
Hide resolved
Updates from review
Updated MultiPartByteRanges
Removed MultiPart from DelayedHandler
Improved the multipart from methods
Improved the multipart from methods
Get attribute from ServletContextRequest
Fixed parts echo test Tested with simple preload handler
removed usage of DelayedHandler
ServletFormHandler
Converted FormFields
* An example usage to asynchronously read UTF-8 content is: | ||
* </p> | ||
* <pre> | ||
* public static class FutureUtf8String extends ContentSourceCompletableFuture<String> |
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.
You need to replace <
and >
with <
and >
respectively in the template type declaration.
return from(request, request, charset, maxFields, maxLength); | ||
} | ||
|
||
static CompletableFuture<Fields> from(Content.Source source, Attributes attributes, Charset charset, int maxFields, int maxLength) |
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.
This needs some serious javadoc'ing.
@@ -138,6 +140,7 @@ private static int getRequestAttribute(Request request, String attribute) | |||
|
|||
public FormFields(Content.Source source, Charset charset, int maxFields, int maxSize) |
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.
Since it looks like this class is meant to be instantiated with the static from*()
methods, maybe the ctor should be private?
@@ -33,7 +35,7 @@ | |||
* A {@link CompletableFuture} that is completed once a {@code application/x-www-form-urlencoded} | |||
* content has been parsed asynchronously from the {@link Content.Source}. | |||
*/ | |||
public class FormFields extends CompletableFuture<Fields> implements Runnable | |||
public class FormFields extends ContentSourceCompletableFuture<Fields> |
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.
All the static
methods lack javadoc.
jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/FormFieldsTest.java
Outdated
Show resolved
Hide resolved
* @return The parsed {@code X} instance or null if parsing is not yet complete | ||
* @throws Throwable Thrown if there is an error parsing | ||
*/ | ||
protected abstract X parse(Content.Chunk chunk) throws Throwable; |
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.
That method is supposed to do both parsing and accumulation. I'm tempted to suggest a new name like parseAndAccumulate
or simply accumulate
.
At least, this should be more clearly javadoc'ed.
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.
I think it just parses... and many parsers have an element of accumulation. However, an implementation is free to call chunk.retain rather than accumulate. I'll javadoc that.
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.
Retaining the chunks is a form of accumulation, but you're right that parsing implicitly implies some sort of accumulation too.
Ok to keep the parse
name, and a bit of javadoc stating this method is supposed to do some accumulation should be enough.
* {@link org.eclipse.jetty.io.Content.Chunk#isFailure(Content.Chunk) failure chunk} | ||
* @return True if the chunk can be ignored. | ||
*/ | ||
protected boolean ignoreTransientFailure(Throwable cause) |
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.
I would not expose this for now, so I'd prefer to see this method be private
.
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.
The whole point of these changes is to allow us to experiment with transient failures.
Specifically, it is removing many different read loops that might not be transient failure safe, and replacing them with this loop that is.
jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java
Show resolved
Hide resolved
...ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/PreloadFormHandler.java
Outdated
Show resolved
Hide resolved
if (cause instanceof IOException ioException) | ||
throw ioException; | ||
|
||
throw new ServletException(new BadMessageException("bad multipart", cause)); |
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.
Why not directly throwing BadMessageException
? Wrapping it in ServletException
to later unwrap it looks strange.
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.
Because Servlet API says that is what we should throw to allow app to catch and handle. We wrap a BadMessage to make sure we send a 400 if the app does not catch.
Yes it is cumbersome... and probably fragile with regards to the TCK for failures.
jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartByteRanges.java
Outdated
Show resolved
Hide resolved
...core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceCompletableFuture.java
Outdated
Show resolved
Hide resolved
...core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceCompletableFuture.java
Outdated
Show resolved
Hide resolved
{ | ||
if (!super.handle(request, response, callback)) | ||
callback.failed(new IllegalStateException("Not Handled")); | ||
return true; |
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.
Can't we just return the boolean
from super
?
The client made a request to the wrong URI, so I'd say it's a 404.
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.
No, because we have already returned true.
...ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/PreloadFormHandler.java
Outdated
Show resolved
Hide resolved
@@ -155,6 +155,11 @@ public ServletHolder(Class<? extends Servlet> servlet) | |||
setHeldClass(servlet); | |||
} | |||
|
|||
public Object getMultipartConfig() |
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.
Why returning Object
?
Also getMultiPartConfig()
capital P.
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.
I've added the type, but it is lowercase p to match the Servlet API type now: MultipartConfigElement
*/ | ||
public static Parts from(ServletApiRequest request) throws IOException | ||
public static CompletableFuture<Parts> from(ServletRequest servletRequest) |
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.
Should not this be HttpServletRequest
? As parts are HTTP things.
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.
It will have to be-a HttpServletRequest, but just going for the lowest common denominator as elsewhere in the code.
...10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java
Outdated
Show resolved
Hide resolved
|
||
future.whenComplete((result, failure) -> | ||
{ | ||
try |
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.
There must be a check on failure
to not call super if we are already failed.
In case of success, we want to store the parts as an attribute, no?
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.
result or failure are handled by ServletApiRequest#getParts. I'll comment.
jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java
Outdated
Show resolved
Hide resolved
updates from review
updates from review
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.
Almost good to go.
jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java
Show resolved
Hide resolved
jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/FormFieldsTest.java
Outdated
Show resolved
Hide resolved
updates from review
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.
LGTM
Signed-off-by: Simone Bordet <[email protected]>
Currently
MultiPartFormData
is only partially used andMultiPartByteRanges
is not used at all. Both contain duplicate code that does not have a clear split between the responsibilities of managing theContent.Source
, parsing the data and managing theCompletableFuture
.The usage of
MultiPartFormData
is in blocking style any bypasses theContent.Source
. If theDelayedHandler.UntilMultiPartFormData
was also used, then 2 attempts to parse the content would be made, one asynchronous that would be successful, but ignored and the second would just see EOF!This PR introduces a
ContentSourceCompletableFuture
which attempts to extract all the common aspects ofMultiPartFormData
andMultiPartByteRanges
. Once updated, then the usage needs to be updated so that it will follow the pattern oforg.eclipse.jetty.server.FormFields#from(org.eclipse.jetty.server.Request, int, int)
so that content can be read asynchronously before a blocking servlet is invoked. However it needs to be a little more complex so that context or servlet specific configuration can be used.