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

Jetty 12 : precompressed content support for ResourceService #8595

Merged

Conversation

joakime
Copy link
Contributor

@joakime joakime commented Sep 15, 2022

Reenable test cases for precompressed content.

Fix implementation to support precompressed content.

@joakime joakime requested review from sbordet and lorban September 15, 2022 21:32
@joakime joakime self-assigned this Sep 15, 2022
@joakime joakime marked this pull request as ready for review September 17, 2022 14:39
* @param suffix the suffix for the ETag
* @return the weak etag
*/
public static String calcWeakEtag(Path path, String suffix)
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this method accept a Resource instead of a Path?

Copy link
Contributor Author

@joakime joakime Sep 20, 2022

Choose a reason for hiding this comment

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

The weak etag is calculated from the path name + last modified timestamp.

Not all resources have them (eg: MemoryResource has neither).
Can't use a ResourceCollection here either, as the path name and last modified timestamp won't always represent the same path object.
It's better to use Path, as even with ResourceCollection in the mix, we'll get a Path then use it.
This is the same Path that is used by the HttpContent to serve the same content.

BTW, Don't get hung up on MemoryResource, the HttpContentFactory reorg PR coming after this will make the case to remove MemoryResource entirely.

* @param path the path to calculate from
* @return the weak etag
*/
public static String calcWeakEtag(Path path)
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this method accept a Resource instead of a Path?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Different method supports Resource now, this method still exists.

@@ -119,13 +119,13 @@ public HttpField getETag()
@Override
public String getETagValue()
{
return _resource.getWeakETag();
return EtagUtil.calcWeakEtag(_resource.getPath());
Copy link
Contributor

Choose a reason for hiding this comment

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

This would only work with path-backed resources. Shouldn't we be capable of calculating the etag of a potential MemoryResource?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in this PR.

@@ -405,7 +406,7 @@ public class CachedHttpContent implements HttpContent

_lastAccessed = Instant.now();

_etag = CachedContentFactory.this._etags ? new PreEncodedHttpField(HttpHeader.ETAG, resource.getWeakETag()) : null;
_etag = CachedContentFactory.this._etags ? new PreEncodedHttpField(HttpHeader.ETAG, EtagUtil.calcWeakEtag(resource.getPath())) : null;
Copy link
Contributor

Choose a reason for hiding this comment

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

This would only work with path-backed resources. Shouldn't we be capable of calculating the etag of a potential MemoryResource?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's no ETag support for MemoryResource as it exists currently.

The HttpContentFactory reorg PR (after this one) will remove MemoryResource in favor of a StaticHttpContent object fallback instead (which can hold the etag based on the URI and JAR file last timestamp)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in this PR anyway.

assertThat(response, containsHeaderValue(HttpHeader.VARY, "Accept-Encoding"));
assertThat(response, containsHeaderValue(HttpHeader.CONTENT_ENCODING, "br"));
String body = response.getContent();
assertThat(body, containsString("fake br"));
Copy link
Contributor

Choose a reason for hiding this comment

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

Why only fake br?

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 contents don't actually have to be compressed to satisfy the test.
The ResourceService doesn't compress it anyway, it just serves the content as-is from the Resource Base.

Copy link
Contributor

Choose a reason for hiding this comment

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

But it confuses the reader the fact that you test for fake br only, while at the beginning of the test the whole content is fake brotli.
I would test for the whole content, since it's so small, to avoid such scratching head moment from other readers of this test ("why only fake br"?).

@@ -1892,35 +1912,6 @@ public void testDirectoryOfCollections() throws Exception
assertThat(response.getStatus(), is(HttpStatus.OK_200));
}

@Test
public void testEtagIfMatchAlwaysFailsDueToWeakEtag() 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.

Why did you remove this test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a bad/duplicate test.

There are other tests that test the same condition.
testIfEtag() for example.
And those expect a 200 OK, not a 412 precondition failure for the same request.

The origin of this test is unknown.
It is not present in jetty-11.0.x.
I can trace as far back as 04acdb7 but not before then.
It seems to have been created during the hackathon.
But etags and preconditions were not working back then.

Interestingly, the github search also shows nothing for this test case...
https://github.com/eclipse/jetty.project/search?q=testEtagIfMatchAlwaysFailsDueToWeakEtag&type=code

@@ -51,20 +51,19 @@ public HttpField getETag()
@Override
public String getETagValue()
{
//return _content.getResource().getWeakETag(_format.getEtagSuffix());
return null;
return EtagUtil.calcWeakEtag(_content.getResource().getPath(), _format.getEtagSuffix());
Copy link
Contributor

Choose a reason for hiding this comment

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

This would only work with path-backed resources. Shouldn't we be capable of calculating the etag of a potential MemoryResource?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixing etag support

@@ -403,7 +404,7 @@ public class CachedHttpContent implements HttpContent

_lastAccessed = Instant.now();

_etag = CachedContentFactory.this._etags ? new PreEncodedHttpField(HttpHeader.ETAG, resource.getWeakETag()) : null;
_etag = CachedContentFactory.this._etags ? new PreEncodedHttpField(HttpHeader.ETAG, EtagUtil.calcWeakEtag(resource.getPath())) : null;
Copy link
Contributor

Choose a reason for hiding this comment

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

This would only work with path-backed resources. Shouldn't we be capable of calculating the etag of a potential MemoryResource?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixing.

@@ -603,7 +604,7 @@ public class CachedPrecompressedHttpContent extends PrecompressedHttpContent
_content = content;
_precompressedContent = precompressedContent;

_etag = (CachedContentFactory.this._etags) ? new PreEncodedHttpField(HttpHeader.ETAG, _content.getResource().getWeakETag(format.getEtagSuffix())) : null;
_etag = (CachedContentFactory.this._etags) ? new PreEncodedHttpField(HttpHeader.ETAG, EtagUtil.calcWeakEtag(_content.getResource().getPath(), format.getEtagSuffix())) : null;
Copy link
Contributor

Choose a reason for hiding this comment

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

This would only work with path-backed resources. Shouldn't we be capable of calculating the etag of a potential MemoryResource?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixing.

@joakime joakime requested a review from lorban September 20, 2022 15:44
FileTime lastModifiedTime = Files.getLastModifiedTime(_delegate.getResource().getPath());
if (lastModifiedTime.equals(_lastModifiedValue))
if (!_delegate.getResource().exists())
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Aren't these redundant checks now that the constructor checks them?

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

/**
* Utility classes for Etag behaviors
*/
public class EtagUtil
Copy link
Contributor

Choose a reason for hiding this comment

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

Contains more than one utility, so I'd prefer EtagUtils.

Also, add a private empty constructor so it cannot be instantiated.

* @param suffix the suffix for the ETag
* @return the weak etag
*/
public static String calcWeakEtag(Path path, String suffix)
Copy link
Contributor

Choose a reason for hiding this comment

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

Prefix calc implies calculations, which to me implies math operations only.
I think something like compute or resolve instead of calc is more appropriate.

continue; // skip, compressed resource modified before actual resource
if (compressedResource.length() >= resource.length())
continue; // skip, not smaller than actual resource
// add found precompressed format
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove the comments, they are just describing what the code does, and the code is very clear.
Less chances that the comments rot.

if (suffix != null)
b.append(suffix);
b.append('"');
return b.toString();
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reference for this algorithm? Is it defined by some spec? If so, please add a reference to 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.

The Etag is just a string.
The bits and pieces we use are just our take on how to create that string.
I'll add the link to the RFC9110 section in the top level javadoc.

* @param suffix the suffix for the ETag
* @return the weak etag
*/
public static String calcWeakEtag(Path path, String suffix)
Copy link
Contributor

Choose a reason for hiding this comment

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

The original implementation in Resource was not using Path at all, so I have the same concerns: this method should take a Resource, not a Path.

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 Resource version didn't handle non-path/file-system based Resources properly.

Btw, there is a Resource method in this class for working with Resource to HttpField.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's also the problem when working with a ResourceCollection there is a difference in .getPath() and .getLastModified() which doesn't match the Resource actually being served in the HttpContent

assertThat(response, containsHeaderValue(HttpHeader.VARY, "Accept-Encoding"));
assertThat(response, containsHeaderValue(HttpHeader.CONTENT_ENCODING, "br"));
String body = response.getContent();
assertThat(body, containsString("fake br"));
Copy link
Contributor

Choose a reason for hiding this comment

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

But it confuses the reader the fact that you test for fake br only, while at the beginning of the test the whole content is fake brotli.
I would test for the whole content, since it's so small, to avoid such scratching head moment from other readers of this test ("why only fake br"?).

* @return True if the tags are equal.
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3.2">RFC9110: Section 8.8.3.2 - Etag Comparison</a>.
*/
public static boolean match(String etag, String etagWithOptionalSuffix)
Copy link
Contributor

Choose a reason for hiding this comment

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

I would call this method matches because it returns a boolean.
It's kind of a query/question rather than a command.

@joakime joakime requested a review from sbordet September 20, 2022 18:28
Signed-off-by: Joakim Erdfelt <[email protected]>
{
_lastAccessed = NanoTime.now();
return true;
FileTime lastModifiedTime = Files.getLastModifiedTime(_delegate.getResource().getPath());
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this makes caching only work with path-backed resources, shouldn't there be a check in the constructor to prevent accepting those?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, let me fix this code block.

+ Using Resource.lastModified were possible
+ CachingHttpContent has better Resource
  protection, and uses Etag from delegate
+ CachingHttpContent uses delegate where
  possible.
+ Store Etag HttpField in CachingHttpContent
  and allow override in constructor
@joakime joakime requested a review from lorban September 21, 2022 11:57
@joakime joakime merged commit 4fe414a into jetty-12.0.x Sep 21, 2022
@joakime joakime deleted the fix/jetty-12-precompressed-support-resource-service branch September 21, 2022 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants