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

Compatible API header parsing plugin #60516

Closed

Conversation

pgomulka
Copy link
Contributor

@pgomulka pgomulka commented Jul 31, 2020

x-pack RestCompatibilityPlugin defines a function that takes information about RestRequest (Accept, Content-Type and hasContent) and returns a Version object indicating what compatible api version was requested.
Function also validates the values provided and throws ElasticsearchStatusException when incorrect combination of header values was used.

In oss distribution a simplistic version of a function, always returning Version.CURRENT is used.

That function is abstracted as CompatibleVersion functional interface.It is being used in RestController - for routing and in AbstractRestChannel for response shape serialisation. To make it available on AbstractRestChannel , it had to be added to ResourceHandlingHttpChannel

pgomulka and others added 13 commits July 27, 2020 15:27
Some revisions to the idea of using a function
this plugin is used in both RestController as well as
AbstractRestChannel
the best way to make getCompatibleVersion function to be usable in both
places is to inject function into a RestRequest

This means that it has to be available in AbstractHttpServerTransport
and all its subclasses (8 of them)
@pgomulka pgomulka added the WIP label Jul 31, 2020
@pgomulka pgomulka self-assigned this Jul 31, 2020
Copy link
Contributor

@jakelandis jakelandis left a comment

Choose a reason for hiding this comment

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

A few comments around naming and what is getting passed around.

@@ -0,0 +1,17 @@
evaluationDependsOn(xpackModule('core'))
Copy link
Contributor

Choose a reason for hiding this comment

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

the above line should not be necessary.

x-pack/plugin/compat-rest-request/build.gradle Outdated Show resolved Hide resolved
@@ -277,6 +277,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
return builder.value(toString());
}

public Version previousMajor() {
return Version.fromString(this.major - 1 + ".0.0");
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can you cache this is a class variable ? (i.e. only calculate it once with a null check)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hm that would lead to a recursive creation. Maybe we could cache the string to build a previous version major-1+."0.0" or a byte (previousMajor-1)*1000000
then using Version.from* returns a cached instance

throw new IllegalStateException("Only one rest compatibility plugin is allowed");
} else if (restCompatibilityPlugins.size() == 1){
restCompatibleFunction =
restCompatibilityPlugins.get(0);
Copy link
Contributor

Choose a reason for hiding this comment

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

This really isn't a function (ie method reference) it is an instance of the plugin which satisfies the functional interface. I don't think we should pass the plugin instance around.

I think you can just change this to

restCompatibleFunction = (a,b,c) ->
                restCompatibilityPlugins.get(0).getCompatibleVersion(a,b,c);

so that you are passing around the method reference. However, it is a bit confusing to return a method reference that satifies the same interface as the object. Maybe two interfaces (one functional like you have, and another a marker interface to filter the plugins) would read better ? Maybe a functional interface CompatibleVersion (instead of RestCompatibility) , and marker interface RestCompatibilityPlugin ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I gave it a go, let me know if this looks any better.
There is a CompatibleVersion field on rest request, but there is a getCompatibleVersionFunction to return it in order to implement a copying constructor.
There is also a getCompatibleVersion function on the RestRequest object itself which uses the CompatibleVersion field.

It might be a bit confusing, WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

also I am not sure about the marker interface. We still have to define a function on it in order to use it, right?

import org.elasticsearch.common.Nullable;

@FunctionalInterface
public interface RestCompatibility {
Copy link
Contributor

Choose a reason for hiding this comment

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

See other comments about maybe breaking this up into two interfaces and simplifying the method name to just get

@jakelandis
Copy link
Contributor

We need to pass that method reference through quite a bit of code. Since this is really only used in a very limited context I wonder if the consuming code should use some form of SPI to load class with the method reference ? We might end up with a couple instances of the class, or have to do some custom class loading ... @pgomulka - what are your thoughts...is this bleeding too much into other areas of the codebase for the sole purpose of passing it through to where it is used ?

@pgomulka
Copy link
Contributor Author

pgomulka commented Sep 1, 2020

@jakelandis I don't think we can use the SPI classloader. In order to load a class with ServiceLoader it would have to be available on a classpath within the server classloader. Xpack modules also have their own classloaders, so ServiceLoader won't be able to load classes from them (unless we pass the plugin instance inteself to the place where ServiceLoader is used - but that requires the same effort as the solution in this PR)
we discussed this here #60220

@pgomulka pgomulka added the v8.0.0 label Sep 8, 2020
@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-core-infra (:Core/Infra/REST API)

@elasticmachine elasticmachine added the Team:Core/Infra Meta label for core/infra team label Sep 8, 2020
@pgomulka pgomulka removed Team:Core/Infra Meta label for core/infra team WIP labels Sep 8, 2020
@pgomulka
Copy link
Contributor Author

pgomulka commented Sep 8, 2020

@elasticmachine run elasticsearch-ci/2

/**
* @return A function that can be used to determine the requested REST compatible version
*/
private CompatibleVersion getRestCompatibleFunction() {
Copy link
Member

Choose a reason for hiding this comment

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

can we add tests for this? Maybe in NodeTests

server/src/test/java/org/elasticsearch/VersionTests.java Outdated Show resolved Hide resolved
Copy link
Member

@jaymode jaymode left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks for the iterations.

server/src/main/java/org/elasticsearch/node/Node.java Outdated Show resolved Hide resolved
@@ -79,11 +79,18 @@ public boolean isContentConsumed() {
return contentConsumed;
}

// for testing
Copy link
Contributor Author

Choose a reason for hiding this comment

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

also if we make mediatypeparser even more strict, we could remove the RestRequest.parseContentType (line 504) and line 60
WDYT?

Byte aVersion = XContentType.parseVersion(acceptHeader);
byte acceptVersion = aVersion == null ? Version.CURRENT.major : Integer.valueOf(aVersion).byteValue();
Byte cVersion = XContentType.parseVersion(contentTypeHeader);
byte contentTypeVersion = cVersion == null ? Version.CURRENT.major : Integer.valueOf(cVersion).byteValue();
Copy link
Contributor Author

@pgomulka pgomulka Oct 6, 2020

Choose a reason for hiding this comment

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

this only supports versioned media types defined in XContentType so json, yaml, cbor, smile

What if someone sends a request like:

curl --request POST \
  --url http://localhost:9200/_sql \
  --header 'accept: application/vnd.elasticsearch+csv;compatible-with=7' \
  --header 'authorization: Basic ZWxhc3RpYzpwYXNzd29yZA==' \
  --header 'content-type: application/vnd.elasticsearch+json;compatible-with=7' \
  --header 'x-opaque-id: 123' \
  --data '{
  "query": "SELECT * FROM library WHERE release_date < \u00272000-01-01\u0027"
}
'

the current logic is not able to parse accept: application/vnd.elasticsearch+csv;compatible-with=7 so sets acceptVersion = 8 however it can parse content-type: application/vnd.elasticsearch+json;compatible-with=7' so contentTypeVersion = 7 that leads to an exception https://github.com/elastic/elasticsearch/pull/60516/files#diff-793fe1b13ffa171bfc74dab865948b08R47 Content-Type and Accept version requests have to match.

How will clients send these headers? will client be aware to which API should they send versioned media types?
@elastic/es-clients any thoughts?

To make it work now the request would have to be sent without versions.

curl --request POST \
  --url http://localhost:9200/_sql \
  --header 'accept: application/csv' \
  --header 'content-type: application/json' \
  --header 'x-opaque-id: 123' \
  --data '{
  "query": "SELECT * FROM library WHERE release_date < \u00272000-01-01\u0027"
}
'

Copy link
Member

Choose a reason for hiding this comment

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

The clients are running with the assumption that we can send the vendor mimetype to all API's.

That way a client running 7.11 can start sending application/vnd.elasticsearch+json;compatible-with=7 to API's even if that API does not have any special backward compatibility handling yet.

If the user upgrades the server first to 7.11 and the API now does have backward compatibility baked in nothing really changes when the client is updated afterwards.

If the user updates to 8.x before the API gets bwc handling the client will still send and receive v7 mime types.

-> accept: application/vnd.elasticsearch+json;compatible-with=7
-> content-type: application/vnd.elasticsearch+json;compatible-with=7

If the client upgrades to 8.x:

-> accept: application/vnd.elasticsearch+json;compatible-with=8
->  content-type: application/vnd.elasticsearch+json;compatible-with=8

If nothing changes in the server 10.x and the API still has no backward compatibly layer. (and the client is still on 8.x)

-> accept: application/vnd.elasticsearch+json;compatible-with=8
-> content-type: application/vnd.elasticsearch+json;compatible-with=8

Will fail, even if nothing actually changed in between these different versions. Adhering to supported backward compatibility guides.

the current logic is not able to parse accept: application/vnd.elasticsearch+csv;compatible-with=7 so sets acceptVersion = 8 however it can parse content-type: application/vnd.elasticsearch+json;compatible-with=7' so contentTypeVersion = 7

What are the cases where it can happen that json will have a compatible response but csv does not? Don't they flow through the same xcontent builders/parsers?

The behavior of setting acceptVersion to something different then what the user/client explicitly provided should not happen. The resulting version mismatch error will be very confusing.

@pgomulka
Copy link
Contributor Author

closing as the change was implemented in 3 PRs
#64406 Introduce per rest endpoint media types
#64423 Allow registering compatible handlers
#64481 Introduce Compatible Version plugin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
:Core/Infra/REST API REST infrastructure and utilities v8.0.0-alpha1
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants