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

Content.Chunk.isTerminal() cannot discriminate EOF from chunks containing a pooled empty buffer #8993

Closed
lorban opened this issue Dec 1, 2022 · 8 comments · Fixed by #8994 or #9073
Assignees
Labels
Bug For general bugs on Jetty side

Comments

@lorban
Copy link
Contributor

lorban commented Dec 1, 2022

Jetty version(s)
12.0.x

Description
Content.Chunk.isTerminal() has a contract that allows some ambiguity: it is not possible to distinguish a Content.Chunk that contains a pool-acquired zero-byte buffer and is last from Content.Chunk.EOF.

Since Content.Chunk.EOF.retain() throws UnsupportedOperationException, each time a new chunk is read and before it is wrapped, it must be checked to make sure it isn't EOF as retain() cannot be called on the latter.

We currently use isTerminal() to perform that check but since it cannot discriminate EOF from an empty-and-last chunk we either leak the chunk's buffer or double-release it.

The Content.Chunk creation, isTerminal() and Retainable contracts should be reworked to lift this ambiguity.

@lorban lorban added Bug For general bugs on Jetty side jetty 12 labels Dec 1, 2022
@lorban lorban self-assigned this Dec 1, 2022
@lorban lorban changed the title Content.Chunk.isTerminal() cannot discriminate EOF from chunks with pooled empty buffer Content.Chunk.isTerminal() cannot discriminate EOF from chunks containing a pooled empty buffer Dec 1, 2022
@lorban
Copy link
Contributor Author

lorban commented Dec 1, 2022

I can see two paths to solve this problem:

  1. The loosening way. Change Content.Chunk.EOF so that it allows calling retain() and always call retain()/release() on all chunks.
  2. The hardening way. Make sure we never create Content.Chunk instances that are empty-and-last and always use EOF in that case.

(1) requires a much looser description of the Retainable contract as each call to retain() doesn't need to be paired with a release() call before the latter returns true.

(2) requires a stricter Content.Chunk creation contract that specifies that passing empty buffer when last is true isn't legal, possibly with some code assertions.

Both options probably have an impact on Content.Chunk.EMPTY and Content.Chunk.Error, this needs to be investigated too.

lorban added a commit that referenced this issue Dec 1, 2022
… Chunk.isTerminal() contract stricter

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
@lorban
Copy link
Contributor Author

lorban commented Dec 1, 2022

Option (2) seems to be the preferred one as it clearly requires the least amount of rework: the server code already assumes this contract.

Required changes needed to adapt the client code are rather minimal.

lorban added a commit that referenced this issue Dec 1, 2022
… Chunk.isTerminal() contract stricter

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
@gregw
Copy link
Contributor

gregw commented Dec 1, 2022

Why does retain on EOF throw? Surely retain and release on a chunk with no buffers are just noops?

Note eitherway, i still like the PR to use EOF when possible

@lorban
Copy link
Contributor Author

lorban commented Dec 2, 2022

The main reason why it was decided that calling retain on EOF would throw is because it isn't possible to properly implement the retain/release contract w.r.t the boolean returned by release().

The root cause of this bug is that the current API doesn't allow implementing a noop retain/release (the Retainable javadoc is too strict) but it's not possible to discriminate chunks upon which we cannot call retain. This is why I listed the two options: either loosen the Retainable contract or harden Content.Chunk so that it doesn't allow creating instances upon which retain cannot be called, forcing the use of the EOF constant, and making isTerminal() reliably detect such case.

Most server-side usages have respected the implicit rule of do-not-create-empty-buffer-last-chunks-always-use-EOF-instead which is why I think making it explicit in the contract and adding some safety checks is the right way forward.

lorban added a commit that referenced this issue Dec 2, 2022
…+ clarify the write(boolean, ByteBuffer, Callback) contract + add test

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
@lorban lorban linked a pull request Dec 2, 2022 that will close this issue
lorban added a commit that referenced this issue Dec 2, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 2, 2022
…as a parameter

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 2, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 2, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 2, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 2, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 2, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 2, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 2, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
@gregw
Copy link
Contributor

gregw commented Dec 4, 2022

Wouldn't a noop implementation of Retainable be:

    class Noop implements Retainable
    {
        @Override
        public void retain()
        {
        }

        @Override
        public boolean release()
        {
            return true;
        }
    }

With that, we don't need to throw from EOF retain (as there is nothing to retain).

However, I still think it is worthwhile to use EOF where possible, so this PR is good regardless. So I think we should do both: loosen the Retainable contract so a noop impl is possible, but also harden our usage/utilities of/for Chunk so that EOF is used when possible.

lorban added a commit that referenced this issue Dec 5, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
@lorban
Copy link
Contributor Author

lorban commented Dec 5, 2022

@gregw I wasn't against that idea at first, quite the opposite. But @sbordet managed to convince me that there was not any good use-case for a noop impl as it always is a missed opportunity for using EOF AFAICT.

lorban added a commit that referenced this issue Dec 5, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
@gregw
Copy link
Contributor

gregw commented Dec 5, 2022

@gregw I wasn't against that idea at first, quite the opposite. But @sbordet managed to convince me that there was not any good use-case for a noop impl as it always is a missed opportunity for using EOF AFAICT.

@sbordet can you convince me too? This is not a show stopper for me, but I do think the code would be cleaner if we didn't have to worry about not being able to retain some types of chunks.

lorban added a commit that referenced this issue Dec 6, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 6, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 6, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 6, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 6, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 6, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 6, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 6, 2022
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 6, 2022
… Chunk.isTerminal() contract stricter

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 6, 2022
… Chunk.isTerminal() contract stricter

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban added a commit that referenced this issue Dec 6, 2022
… Chunk.isTerminal() contract stricter

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
@sbordet sbordet linked a pull request Dec 25, 2022 that will close this issue
sbordet added a commit that referenced this issue Jan 3, 2023
* Restored Jetty 11's AsyncContentProducer and related classes in jetty-ee9-nested module (src and test).
* Introduced Retainable.canRetain().
* Removed Chunk.isTerminal() and replaced it with alternative method calls.
* Clarified AsyncContent.write() in case of Chunk.Error.
* Removed AsyncContent.write(Chunk, Callback) because it was making the API confusing.
  For example, AsyncContent.close() clashing with write(EOF, NOOP), or
  AsyncContent.fail(x) clashing with write(Chunk.Error, NOOP), etc.
* Improved usage of Chunk.from(..., Retainable).
* Improved usage of Chunk.slice().

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
sbordet added a commit that referenced this issue Jan 13, 2023
* Fixes #8993 - Retainability of special Chunks

* Restored Jetty 11's AsyncContentProducer and related classes in jetty-ee9-nested module (src and test).
* Introduced Retainable.canRetain().
* Removed Chunk.isTerminal() and replaced it with alternative method calls.
* Clarified AsyncContent.write() in case of Chunk.Error.
* Removed AsyncContent.write(Chunk, Callback) because it was making the API confusing.
  For example, AsyncContent.close() clashing with write(EOF, NOOP), or
  AsyncContent.fail(x) clashing with write(Chunk.Error, NOOP), etc.
* Improved usage of Chunk.from(..., Retainable).
* Improved usage of Chunk.slice().
* Using from() in MultiPart, rather than duplicating code.
* Fixed MultiPart.Parser.Listener.onPartContent() javadocs.
* Renamed non-retaining Chunk.from() to Chunk.asChunk().
* Removed Chunk.slice() methods, inlining them where necessary.
* Carefully reviewed all usages of read()-like methods that return a Retainable instance to make sure it is released.
* Updated HTTP/2 and HTTP/3 usages of Stream.Data to follow the correct retain/release semantic.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
@sbordet
Copy link
Contributor

sbordet commented Jan 13, 2023

Fixed by #9073 and #9159.

@sbordet sbordet closed this as completed Jan 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug For general bugs on Jetty side
Projects
None yet
3 participants