Conversation
… and HttpOutput Signed-off-by: Ludovic Orban <lorban@bitronix.be>
|
Is this a better (generic) replacement for PR #15433 ? Seems like this will apply to all compression libs equally. |
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
…678-ServletContextResponse-HttpOutput-linking
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
…fer test Signed-off-by: Ludovic Orban <lorban@bitronix.be>
…) call Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
…to confirm they are the ones completing the HttpOutput, hence doing the last write Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
gregw
left a comment
There was a problem hiding this comment.
Early feedback that I don't like the name at least. I need to understand more before I can say if the approach is OK.
| content.writeTo(_servletChannel.getResponse(), last, callback); | ||
| } | ||
|
|
||
| void lastWriteComplete() |
There was a problem hiding this comment.
This is badly named and it is nasty that it needs to be protected.
If I'm reading the code correctly it is called by the Servlet streams before the last write is done. So it is kind of a notification that the last write will be done? But then it is calling the callback before the write is done?
So it needs a better name at least... but a solution that doesn't need this call at all would be better. Can you add some more detail in the PR about what the actual issue is and how this solves it?
There was a problem hiding this comment.
This is called by the ServletContextResponse when it is called by the Core API the last write is being done but the HttpOutput has not yet been closed.
I've updated the PR with a better description of the problem, as accurately as my mind reminds it. Hopefully that should help you better understand the context.
| if (!httpOutput.isClosed() && last) | ||
| httpOutput.lastWriteComplete(); | ||
| httpOutput.addBytesWritten(BufferUtil.length(content)); |
There was a problem hiding this comment.
You should be able to pass all the state you need here simply with:
| if (!httpOutput.isClosed() && last) | |
| httpOutput.lastWriteComplete(); | |
| httpOutput.addBytesWritten(BufferUtil.length(content)); | |
| httpOutput.addBytesWritten(BufferUtil.length(content), last); |
There was a problem hiding this comment.
Actually, you really should not be messing with this written mechanism. It is there for some very specific TCK test that check that we automagically close when the last byte is written. This is above the level of compression, so it matches any application concept of content-length with the bytes written.
Compression will change both.
So we need to better understand the problem as this looks like the wrong solution.
There was a problem hiding this comment.
I did not think of looking at the TCK tests, but yes, I should as this mechanism is quite sensible. Any hints about which ones to look at from the top of your mind?
About this new accounting, it is at the root of this bug: if zero byte is written via the HttpOutput but some were written directly via the servlet response, the servlet channel will believe that the written byte count does not match the Content-Length header's value that was previously set by ResourceService.sendData(), intercepted by ServletContextResponse.HttpFieldsWrapper.onAddField() and fed to HttpOutput.setApplicationContentLength() so ServletChannel.handle()'s getHttpOutput().isContentIncomplete() check returens false and the response gets failed.
| private long lockedGetWritten() | ||
| { | ||
| assert _channelState.isLockHeldByCurrentThread(); | ||
| return _written + (_aggregate == null ? 0 : _aggregate.remaining()); |
There was a problem hiding this comment.
This looks to be double counting. The aggregated bytes includes those that are written. I.e. how does a byte get aggregated without being written?
There was a problem hiding this comment.
This method returns the # of bytes that have been written, either via HttpOutput (that may have buffered some if the written data) or via ServletContextResponse.write(), which is slightly different from the # of bytes already flushed, hence the addition of the yet-to-be-written bytes buffered by the HttpOutput's aggregation.
| if (!httpOutput.isClosed() && last) | ||
| httpOutput.lastWriteComplete(); | ||
| httpOutput.addBytesWritten(BufferUtil.length(content)); |
There was a problem hiding this comment.
Actually, you really should not be messing with this written mechanism. It is there for some very specific TCK test that check that we automagically close when the last byte is written. This is above the level of compression, so it matches any application concept of content-length with the bytes written.
Compression will change both.
So we need to better understand the problem as this looks like the wrong solution.
|
@lorban I can get the test harness to pass with the simple addition of: @Override
public void write(boolean last, ByteBuffer content, Callback callback)
{
getHttpOutput().addBytesWritten(content.remaining());
super.write(last, content, callback);
}Which is using protected WriteRecord encode(boolean last, ByteBuffer content)
{
State initialState = state.get();
if (initialState == State.FINISHED)
{
if (content.hasRemaining())
throw new IllegalStateException("Already released " + BufferUtil.toDetailString(content));
return null;
}
...That second part might be a hack, as it would be better to stop the iteration prior to calling encode again with an empty content. I'm not sure why it is doing that, so perhaps a better fix is to stop that. Eitherway, I think a much simpler fix is possible. |
|
@gregw with your suggested fix you get 8 failures in
|
Still, I think it is the correct approach. The addBytesWritten API is there precisely to update the count when an alternate path is taken. I think your approach is messing with that too much and it is very unclear exactly what it is trying to do (See my first comments) I'll try to find some time this week to look some more. |
|
@lorban I'm now thinking the fix really needs to be in ResourceServlet. It is the one that decides to bypass the ServletResponse and call the core response directly, so it should be responsible for making sure the accounting is done correctly. I'm going to play there for a bit. However, there is a problem with either compression or the test (@joakime you might want to comment on this). If I fiddle with ResourceServlet so that it always uses the ServletResponse, then I get the exception: I can avoid this by making encode just return null if the passed content is empty and we are last. But we really should not need to do that?? Or should we??? |
|
@lorban I made the change to ResourceServlet to make it do its own accounting and it does fix the problem. I think the approach needs refinements, but I think it is correct. |
|
@gregw I also think the There are basically 3 changes in this PR:
(1) I think we're in agreement: I'm going to work on removing the changes related to (3) entirely to see if the whole Jetty test suite passes without them, or point out what fails. I'm also going to have a look at the servlet TCK to see how it goes with and without these changes. |
I don't get why you'd want to modify the |
Because there are only two options:
In fact, I don't think you can "fix ServletContextResponse |
Update: calling |
If we were discussing unwrapping But in this case, the |
|
@lorban said:
But I guess we could move the accounting to So the However you think of the bypass, it is a bypass. In this case it is bypassing the hard requirement for us to count bytes at the API level, not at the lower levels. I think the bypasser needs to be responsible. |
|
|
@sbordet I'm all for avoiding code duplication and putting the bypass code somewhere common, but.... I do not see how that can be done in You can see that the bypass code in I think we should do the simple fix for now, to just fix this issue. Then look at the common code fix later. Putting the common code on |
|
@gregw I've taken your branch, added the missing fixes for the Basically, if you agree in substance with that other branch, the only disagreement left is where to put the missing |
|
Superseded by #15847 |
ServletContextResponseandHttpOutputshould be kept in sync w.r.t how many bytes were written (to apply the content-length logic) and their closed state / last flag handling.Problem Description
The problem is exposed by the newly added
ResourceServletCompressionCompleteTest: when a Core handler (CompressionHandlerwith zstd in this case, but any other handler modifying the output would trigger the bug) wraps a EE10/11ServletContextHandler, any sufficiently large data (larger than the output buffer size) sent by the servlet (ResourceServletserving a 64 KB file in this case) result in the output being corrupted and theServletChannelaborting withjava.io.IOException: Insufficient content written 0 < 65536.This happens because the data is sent via the Core
Response.write()API, bypassing theHttpOutputentirely such as it did not see a single byte being written, so when theServletChannelcallsHttpOutput.isContentIncomplete()that method returnstrueand the servlet channel aborts the request, thinking no byte was written.This change fixes the problem by ensuring 3 things:
ServletContextResponse.write()is accounted for inHttpOutput.InputStreamWritingCBandReadableByteChannelWritingCBso that the straight-throughHttpOutput.sendContent()calls can't deadlock.HttpOutputwhenServletContextResponse.write()sees the last write being written.Fixes #15678