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

Allow registering compatible handlers #64423

Merged
merged 34 commits into from
Nov 16, 2020
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e468fdf
Introduce per endpoint media types
pgomulka Oct 30, 2020
5cfa11e
Merge branch 'master' into compat/parsed_media_type
pgomulka Oct 30, 2020
66224e7
allow smile and cbor to parse charset param. See ClientYamlTestExecut…
pgomulka Oct 30, 2020
29b27e8
javadocs
pgomulka Oct 30, 2020
4690362
fix javadoc
pgomulka Oct 30, 2020
e7866a9
Allow registration of compatible version-1 handlers
pgomulka Oct 30, 2020
0330d97
add tests
pgomulka Nov 2, 2020
78d4660
Merge branch 'master' into compat/register_compatible_handlers
pgomulka Nov 2, 2020
bc5de8b
pass version to xcontentbuilder
pgomulka Nov 2, 2020
f958cdd
Merge branch 'master' into compat/register_compatible_handlers
pgomulka Nov 2, 2020
2bda74f
code review follow up
pgomulka Nov 3, 2020
7779083
minor tweaks
pgomulka Nov 3, 2020
dd26408
Merge branch 'master' into compat/introduce_per_endpoint_media_types
pgomulka Nov 3, 2020
94b4a0a
fix test after exception msg rename
pgomulka Nov 3, 2020
4385591
rename to header value
pgomulka Nov 3, 2020
dc61731
remove charset validation
pgomulka Nov 4, 2020
4a3138d
Apply suggestions from code review
pgomulka Nov 5, 2020
0b565e5
javadoc
pgomulka Nov 5, 2020
b35ad2c
Merge branch 'master' into compat/introduce_per_endpoint_media_types
pgomulka Nov 5, 2020
b908bfe
Merge branch 'compat/introduce_per_endpoint_media_types' of github.co…
pgomulka Nov 5, 2020
950ba7b
Merge branch 'compat/introduce_per_endpoint_media_types' into compat/…
pgomulka Nov 5, 2020
310b735
Merge branch 'master' into compat/register_compatible_handlers
pgomulka Nov 5, 2020
afba70d
javadoc and cleanup
pgomulka Nov 5, 2020
385f5a9
Merge branch 'master' into compat/register_compatible_handlers
pgomulka Nov 6, 2020
836fe0f
tests and javadoc
pgomulka Nov 6, 2020
8f2ea92
javadoc
pgomulka Nov 9, 2020
d70a071
do not set compatible version twice
pgomulka Nov 9, 2020
87d762f
Merge branch 'master' into compat/register_compatible_handlers
pgomulka Nov 9, 2020
deff739
javadocs
pgomulka Nov 9, 2020
4ecbf22
Merge remote-tracking branch 'upstream/master' into compat/register_c…
pgomulka Nov 10, 2020
a38ed04
Merge remote-tracking branch 'upstream/master' into compat/register_c…
pgomulka Nov 10, 2020
c39d0bb
Apply suggestions from code review
pgomulka Nov 10, 2020
09704f3
Merge branch 'compat/register_compatible_handlers' of github.com:pgom…
pgomulka Nov 10, 2020
023c8d9
xcontentbuilder has the version requested by a user
pgomulka Nov 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
*/
public final class XContentBuilder implements Closeable, Flushable {

private byte compatibleMajorVersion;
Copy link
Member

Choose a reason for hiding this comment

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

hmm, I wonder if we should make this a SetOnce or add validation that if it is not some sentinel value that it cannot be changed again? I'm not sure that I like it being mutable with a builder pattern and allowing it to be set multiple times

Copy link
Contributor Author

Choose a reason for hiding this comment

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

with a regular builder pattern I would be ok with the field being mutable, as it is normally used within some narrow code scope.
With XContentBuilder we often pass it around so makes sense to protect against some accidental changes.
I feel like we should assert about this in testing only though.
I added assert


/**
* Create a new {@link XContentBuilder} using the given {@link XContent} content.
* <p>
Expand Down Expand Up @@ -1004,6 +1006,15 @@ public XContentBuilder copyCurrentStructure(XContentParser parser) throws IOExce
return this;
}

public XContentBuilder withCompatibleMajorVersion(byte compatibleMajorVersion){
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public XContentBuilder withCompatibleMajorVersion(byte compatibleMajorVersion){
public XContentBuilder withCompatibleMajorVersion(byte compatibleMajorVersion) {

this.compatibleMajorVersion = compatibleMajorVersion;
return this;
}

public byte getCompatibleMajorVersion() {
Copy link
Member

Choose a reason for hiding this comment

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

can you add javadocs to this method and the one above?

return compatibleMajorVersion;
}

@Override
public void flush() throws IOException {
generator.flush();
Expand Down
10 changes: 10 additions & 0 deletions server/src/main/java/org/elasticsearch/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ private static Version fromStringSlow(String version) {
public final byte build;
public final org.apache.lucene.util.Version luceneVersion;
private final String toString;
public final int previousMajorId;
pgomulka marked this conversation as resolved.
Show resolved Hide resolved

Version(int id, org.apache.lucene.util.Version luceneVersion) {
this.id = id;
Expand All @@ -268,6 +269,15 @@ private static Version fromStringSlow(String version) {
this.build = (byte) (id % 100);
this.luceneVersion = Objects.requireNonNull(luceneVersion);
this.toString = major + "." + minor + "." + revision;
this.previousMajorId = major > 0 ? (major - 1) * 1000000 + 99 : major;
}

public Version previousMajor() {
Copy link
Member

Choose a reason for hiding this comment

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

javadocs please

return Version.fromId(previousMajorId);
}

public static Version minimumRestCompatibilityVersion() {
Copy link
Member

Choose a reason for hiding this comment

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

hmm, should we make this non-static like minimumCompatibilityVersion and minimumIndexCompatibilityVersion? Also, please add javadocs :)

return Version.CURRENT.previousMajor();
}

public boolean after(Version version) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@
import org.elasticsearch.persistent.UpdatePersistentTaskStatusAction;
import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.plugins.ActionPlugin.ActionHandler;
import org.elasticsearch.rest.CompatibleVersion;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestHandler;
import org.elasticsearch.rest.RestHeaderDefinition;
Expand Down Expand Up @@ -427,7 +428,8 @@ public class ActionModule extends AbstractModule {
public ActionModule(Settings settings, IndexNameExpressionResolver indexNameExpressionResolver,
IndexScopedSettings indexScopedSettings, ClusterSettings clusterSettings, SettingsFilter settingsFilter,
ThreadPool threadPool, List<ActionPlugin> actionPlugins, NodeClient nodeClient,
CircuitBreakerService circuitBreakerService, UsageService usageService, SystemIndices systemIndices) {
CircuitBreakerService circuitBreakerService, UsageService usageService, SystemIndices systemIndices,
CompatibleVersion compatibleVersion) {
this.settings = settings;
this.indexNameExpressionResolver = indexNameExpressionResolver;
this.indexScopedSettings = indexScopedSettings;
Expand Down Expand Up @@ -459,7 +461,7 @@ public ActionModule(Settings settings, IndexNameExpressionResolver indexNameExpr
indicesAliasesRequestRequestValidators = new RequestValidators<>(
actionPlugins.stream().flatMap(p -> p.indicesAliasesRequestValidators().stream()).collect(Collectors.toList()));

restController = new RestController(headers, restWrapper, nodeClient, circuitBreakerService, usageService);
restController = new RestController(headers, restWrapper, nodeClient, circuitBreakerService, usageService, compatibleVersion);
}


Expand Down
14 changes: 13 additions & 1 deletion server/src/main/java/org/elasticsearch/node/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
import org.elasticsearch.plugins.SystemIndexPlugin;
import org.elasticsearch.repositories.RepositoriesModule;
import org.elasticsearch.repositories.RepositoriesService;
import org.elasticsearch.rest.CompatibleVersion;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;
Expand Down Expand Up @@ -539,9 +540,11 @@ protected Node(final Environment initialEnvironment,
repositoriesServiceReference::get).stream())
.collect(Collectors.toList());


Copy link
Member

Choose a reason for hiding this comment

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

Suggested change

ActionModule actionModule = new ActionModule(settings, clusterModule.getIndexNameExpressionResolver(),
settingsModule.getIndexScopedSettings(), settingsModule.getClusterSettings(), settingsModule.getSettingsFilter(),
threadPool, pluginsService.filterPlugins(ActionPlugin.class), client, circuitBreakerService, usageService, systemIndices);
threadPool, pluginsService.filterPlugins(ActionPlugin.class), client, circuitBreakerService, usageService, systemIndices,
getRestCompatibleFunction());
modules.add(actionModule);

final RestController restController = actionModule.getRestController();
Expand Down Expand Up @@ -716,6 +719,15 @@ protected Node(final Environment initialEnvironment,
}
}

/**
* @return A function that can be used to determine the requested REST compatible version
* package scope for testing
*/
CompatibleVersion getRestCompatibleFunction() {
// TODO PG Until compatible version plugin is implemented, return current version.
return CompatibleVersion.CURRENT_VERSION;
}

protected TransportService newTransportService(Settings settings, Transport transport, ThreadPool threadPool,
TransportInterceptor interceptor,
Function<BoundTransportAddress, DiscoveryNode> localNodeFactory,
Expand Down
35 changes: 35 additions & 0 deletions server/src/main/java/org/elasticsearch/rest/CompatibleVersion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.rest;

import org.elasticsearch.Version;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.xcontent.ParsedMediaType;

/**
* An interface used to specify a function that returns a compatible API version
* Intended to be used in a code base instead of a plugin.
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure i follow the second sentence (Intended to be ...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

right.. I rephrased this, but maybe it is still to vague. let me know
My intention was to not describe the actual plugin implementation, but just to mention that this abstract the way we actually provide the logic.
It happens that we use a plugin for this, but could if we want to use any class within a server to do this as well (we did in the previous attempts)

*/
@FunctionalInterface
public interface CompatibleVersion {
Version get(@Nullable ParsedMediaType acceptHeader, @Nullable ParsedMediaType contentTypeHeader, boolean hasContent);

CompatibleVersion CURRENT_VERSION = (acceptHeader, contentTypeHeader, hasContent) -> Version.CURRENT;
}
28 changes: 18 additions & 10 deletions server/src/main/java/org/elasticsearch/rest/MethodHandlers.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

package org.elasticsearch.rest;

import org.elasticsearch.common.Nullable;
import org.elasticsearch.Version;

import java.util.HashMap;
import java.util.Map;
Expand All @@ -31,23 +31,25 @@
final class MethodHandlers {

private final String path;
private final Map<RestRequest.Method, RestHandler> methodHandlers;
private final Map<RestRequest.Method, Map<Version, RestHandler>> methodHandlers;

MethodHandlers(String path, RestHandler handler, RestRequest.Method... methods) {
MethodHandlers(String path, RestHandler handler, Version version, RestRequest.Method... methods) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you a few unit tests for this class ?

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 also realised that changing the method signature was not necessary.

this.path = path;
this.methodHandlers = new HashMap<>(methods.length);
for (RestRequest.Method method : methods) {
methodHandlers.put(method, handler);
methodHandlers.computeIfAbsent(method, k -> new HashMap<>())
.put(version, handler);
}
}

/**
* Add a handler for an additional array of methods. Note that {@code MethodHandlers}
* does not allow replacing the handler for an already existing method.
*/
MethodHandlers addMethods(RestHandler handler, RestRequest.Method... methods) {
MethodHandlers addMethods(RestHandler handler, Version version, RestRequest.Method... methods) {
for (RestRequest.Method method : methods) {
RestHandler existing = methodHandlers.putIfAbsent(method, handler);
RestHandler existing = methodHandlers.computeIfAbsent(method, k -> new HashMap<>())
.putIfAbsent(version, handler);
if (existing != null) {
throw new IllegalArgumentException("Cannot replace existing handler for [" + path + "] for method: " + method);
}
Expand All @@ -56,11 +58,17 @@ MethodHandlers addMethods(RestHandler handler, RestRequest.Method... methods) {
}

/**
* Returns the handler for the given method or {@code null} if none exists.
* Returns the handler for the given method and version.
* If a handler for given version do not exist, a handler for Version.CURRENT will be returned.
* or {@code null} if none exists.
*/
@Nullable
RestHandler getHandler(RestRequest.Method method) {
return methodHandlers.get(method);
RestHandler getHandler(RestRequest.Method method, Version version) {
Map<Version, RestHandler> versionToHandlers = methodHandlers.get(method);
if (versionToHandlers == null) {
return null; //method not found
}
final RestHandler handler = versionToHandlers.get(version);
return handler != null || version.equals(Version.CURRENT) ? handler : versionToHandlers.get(Version.CURRENT);
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 really can't remember the use cases where we needed this. (returning a v8 handler when a handler was not present in v7)
@jaymode made a comment about this when we were still working on a feature branch https://github.com/elastic/elasticsearch/pull/54197/files#r414959451

Copy link
Member

Choose a reason for hiding this comment

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

I think the reason for this is @Mpdreamz's comment #60516 (comment) where clients plan to send compatible with to all APIs. If this is the case, can we please document this in code/method so we do not lose the information

Copy link
Contributor

Choose a reason for hiding this comment

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

I had to think about this waaay to long :) and wrote a unit test to help me understand

    /**
     * We want clients to be able to request compatibility with a prior version for all APIs. If a there does not exist a handler registered
     * for the prior version, we want to return the current version handler. This effectively means that the current endpoint is compatible
     * with the prior version, which should be true since if it were not compatible there would be a registered compatible endpoint.
     */
    public void testMissingPriorHandlerReturnsCurrentHandler(){
        RestHandler currentVersionHandler = new CurrentVersionHandler();
        MethodHandlers methodHandlers = new MethodHandlers("path", currentVersionHandler, RestRequest.Method.PUT, RestRequest.Method.POST);
        RestHandler handler = methodHandlers.getHandler(RestRequest.Method.PUT, Version.CURRENT.previousMajor());
        assertThat(handler, sameInstance(currentVersionHandler));
    }

Can you add a similar comment inline here, and add that test?

Copy link
Contributor

Choose a reason for hiding this comment

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

The return can be simplified to:

        return handler == null ? versionToHandlers.get(Version.CURRENT) : handler;

with a test:

    public void testMissingCurrentHandler(){
        RestHandler previousVersionHandler = new PreviousVersionHandler();
        MethodHandlers methodHandlers = new MethodHandlers("path", previousVersionHandler, RestRequest.Method.PUT, RestRequest.Method.POST);
        RestHandler handler = methodHandlers.getHandler(RestRequest.Method.PUT, Version.CURRENT);
        assertNull(handler);
    }

Copy link
Contributor Author

@pgomulka pgomulka Nov 9, 2020

Choose a reason for hiding this comment

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

to be fair, I am not super convinced about returning the CURRENT.
if an endpoint did not exist in v7 server version, client would be getting 404 when trying to access it with v7 client.
When he upgrades server to next version and he still uses v7 client, I guess he should be still getting 404?

@jakelandis I am not sure I understand this:

This effectively means that the current endpoint is compatible
* with the prior version, which should be true since if it were not compatible there would be a registered compatible endpoint.

from high level point of view, there is no knowledge if there is a compatible endpoint or not. If the handler's code did not change, user has no idea that a response came from v7 code, or if the response is compatible shape from v8 (in ideal 100% compatible handler).

THe same for when an endpoint do not exist. Returning 404 from v8 when for not existing in v7 endpoint is 100% compatible in shape and behaviour

@Mpdreamz any views on this?

Copy link
Contributor

@jakelandis jakelandis Nov 9, 2020

Choose a reason for hiding this comment

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

I think we should return new APIs with compatible requested. Below is a table with my thought for how behaviour should look.

The rationale is that in a minor you can add a new API passively, therefor new APIs are compatible (as opposed to non-compatible/breaking). Further, one motivation to add a new API is to replace an old one. With a client sending compatibility all the time, they should be able to fix the compatibility warnings and migrate to the new API. This allows them to do just that.

scenario v7 v8 v7 compat / v8 server result
new in v8 404 200 200 (v8)
no change/compat change in v8 (no compat handler) 200 200 200 (v8)
removed in v8 (with compat handler) 200 404 200 (v7)
non passive change (with compat handler) 200 200 200 (v7)

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 rationale is that in a minor you can add a new API passively, therefor new APIs are compatible (as opposed to non-compatible/breaking)

great explanation. thank you. I think this was the initial reason we went for that behaviour on a feature branch. I will add a comment about this at least in a test

Copy link
Member

@Mpdreamz Mpdreamz Nov 9, 2020

Choose a reason for hiding this comment

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

Since clients generate methods for known API's the only way to talk to an API that exists in 8.0 but not (yet) in 7.x is to call transport.Request(url)

I think the new in v8 case needs some zooming in on:

new in v8

accept/content-type v7 compat / v8 server result
application/json application/json
application/vnd.elasticsearch+json;compatible-with=8 application/vnd.elasticsearch+json;compatible-with=8
application/vnd.elasticsearch+json;compatible-with=7 ?

The first two seem straight forward the last one not so much It could return a

  • 404, since thats what it did in 7
  • 200 with application/vnd.elasticsearch+json;compatible-with=7.
  • 200 with application/vnd.elasticsearch+json;compatible-with=8.

Returning with compatible-with=8 is not what the user requested so seems like an unexpected return.

Returning with compatible-with=7 since its a new API it can be argued anything is compatible with 7. But this will become a pain to track when e.g a new API gets introduced in 8.0 as experimental and GA's in 8.4. You potentially get back two different 7 responses for 8.0 and 8.4.

That leaves returning a 404.

cc @elastic/es-clients we have not discussed if the the transport used in the client should default to application/vnd.elasticsearch+json;compatible-with=7 or application/json. I feel that if the transport defaults to application/json but the API methods calling the transport default to the vendor mime type the client user has the least suprises.

Copy link
Contributor Author

@pgomulka pgomulka Nov 10, 2020

Choose a reason for hiding this comment

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

I think with the current approach where we version compatible API per major we won't be able to give a 100% experience of previous version server. And that wasn't a goal - after all we want to achieve the compatible API, not the versioned one.
I feel all the comments from this "thread" are valid and I will summarise them on a separate issue.
The current implementation is not returning the right response header yet, so the discussion on new in v8 could continue on the new issue.
Lets use this issue to facilitate the discussion #64852

}

/**
Expand Down
42 changes: 32 additions & 10 deletions server/src/main/java/org/elasticsearch/rest/RestController.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.Version;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
Expand Down Expand Up @@ -90,11 +91,14 @@ public class RestController implements HttpServerTransport.Dispatcher {
/** Rest headers that are copied to internal requests made during a rest request. */
private final Set<RestHeaderDefinition> headersToCopy;
private final UsageService usageService;
private CompatibleVersion compatibleVersion;

public RestController(Set<RestHeaderDefinition> headersToCopy, UnaryOperator<RestHandler> handlerWrapper,
NodeClient client, CircuitBreakerService circuitBreakerService, UsageService usageService) {
NodeClient client, CircuitBreakerService circuitBreakerService, UsageService usageService,
CompatibleVersion compatibleVersion) {
this.headersToCopy = headersToCopy;
this.usageService = usageService;
this.compatibleVersion = compatibleVersion;
if (handlerWrapper == null) {
handlerWrapper = h -> h; // passthrough if no wrapper set
}
Expand Down Expand Up @@ -168,8 +172,12 @@ protected void registerHandler(RestRequest.Method method, String path, RestHandl
}

private void registerHandlerNoWrap(RestRequest.Method method, String path, RestHandler maybeWrappedHandler) {
handlers.insertOrUpdate(path, new MethodHandlers(path, maybeWrappedHandler, method),
(mHandlers, newMHandler) -> mHandlers.addMethods(maybeWrappedHandler, method));
final Version version = maybeWrappedHandler.compatibleWithVersion();
assert Version.minimumRestCompatibilityVersion() == version || Version.CURRENT == version
: "REST API compatibility is only supported for version " + Version.minimumRestCompatibilityVersion().major;

handlers.insertOrUpdate(path, new MethodHandlers(path, maybeWrappedHandler, version, method),
(mHandlers, newMHandler) -> mHandlers.addMethods(maybeWrappedHandler, version, method));
}

/**
Expand Down Expand Up @@ -242,7 +250,11 @@ private void dispatchRequest(RestRequest request, RestChannel channel, RestHandl
inFlightRequestsBreaker(circuitBreakerService).addWithoutBreaking(contentLength);
}
// iff we could reserve bytes for the request we need to send the response also over this channel
responseChannel = new ResourceHandlingHttpChannel(channel, circuitBreakerService, contentLength);
// Using a version from a handler because if no handler was found for requested version,
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure i understand the purpose of this comment here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

my intention was to indicate that even if you requested an applicaiton/vnd.elasticsearch+json;compatible-with=7 but the handler did not exist in 7, the actual resthandler will be used from v8.
but that indeed makes no difference, as there would be no compatible serialisation code.
Removed the comment.

// we would return a handler for CURRENT. Therefore no compatible logic in serialisation (toXContent) should be applied
// see MethodHandlers#getHandler
responseChannel = new ResourceHandlingHttpChannel(channel, circuitBreakerService, contentLength,
handler.compatibleWithVersion());
// TODO: Count requests double in the circuit breaker if they need copying?
if (handler.allowsUnsafeBuffers() == false) {
request.ensureSafeBuffers();
Expand Down Expand Up @@ -318,6 +330,9 @@ private void tryAllHandlers(final RestRequest request, final RestChannel channel
final String rawPath = request.rawPath();
final String uri = request.uri();
final RestRequest.Method requestMethod;

Version compatibleVersion = this.compatibleVersion.
get(request.getParsedAccept(), request.getParsedContentType(), request.hasContent());
try {
// Resolves the HTTP method and fails if the method is invalid
requestMethod = request.method();
Expand All @@ -329,7 +344,7 @@ private void tryAllHandlers(final RestRequest request, final RestChannel channel
if (handlers == null) {
handler = null;
} else {
handler = handlers.getHandler(requestMethod);
handler = handlers.getHandler(requestMethod, compatibleVersion);
}
if (handler == null) {
if (handleNoHandlerFound(rawPath, requestMethod, uri, channel)) {
Expand Down Expand Up @@ -454,33 +469,40 @@ private static final class ResourceHandlingHttpChannel implements RestChannel {
private final RestChannel delegate;
private final CircuitBreakerService circuitBreakerService;
private final int contentLength;
private final Version compatibleVersion;
private final AtomicBoolean closed = new AtomicBoolean();

ResourceHandlingHttpChannel(RestChannel delegate, CircuitBreakerService circuitBreakerService, int contentLength) {
ResourceHandlingHttpChannel(RestChannel delegate, CircuitBreakerService circuitBreakerService, int contentLength,
Version compatibleVersion) {
this.delegate = delegate;
this.circuitBreakerService = circuitBreakerService;
this.contentLength = contentLength;
this.compatibleVersion = compatibleVersion;
}

@Override
public XContentBuilder newBuilder() throws IOException {
return delegate.newBuilder();
return delegate.newBuilder()
.withCompatibleMajorVersion(compatibleVersion.major);
}

@Override
public XContentBuilder newErrorBuilder() throws IOException {
return delegate.newErrorBuilder();
return delegate.newErrorBuilder()
.withCompatibleMajorVersion(compatibleVersion.major);
}

@Override
public XContentBuilder newBuilder(@Nullable XContentType xContentType, boolean useFiltering) throws IOException {
return delegate.newBuilder(xContentType, useFiltering);
return delegate.newBuilder(xContentType, useFiltering)
.withCompatibleMajorVersion(compatibleVersion.major);
}

@Override
public XContentBuilder newBuilder(XContentType xContentType, XContentType responseContentType, boolean useFiltering)
throws IOException {
return delegate.newBuilder(xContentType, responseContentType, useFiltering);
return delegate.newBuilder(xContentType, responseContentType, useFiltering)
.withCompatibleMajorVersion(compatibleVersion.major);
}

@Override
Expand Down
11 changes: 11 additions & 0 deletions server/src/main/java/org/elasticsearch/rest/RestHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package org.elasticsearch.rest;

import org.elasticsearch.Version;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.xcontent.MediaType;
import org.elasticsearch.common.xcontent.MediaTypeRegistry;
Expand Down Expand Up @@ -106,6 +107,16 @@ default MediaTypeRegistry<? extends MediaType> validAcceptMediaTypes() {
return XContentType.MEDIA_TYPE_REGISTRY;
}

/**
* Returns a version a handler is compatible with.
* This version is then used to math a handler with a request that specified a version.
* If no version is specified, handler is assumed to be compatible with <code>Version.CURRENT</code>
* @return a version
*/
default Version compatibleWithVersion() {
return Version.CURRENT;
}

class Route {

private final String path;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.plugins.ActionPlugin.ActionHandler;
import org.elasticsearch.rest.CompatibleVersion;
import org.elasticsearch.rest.RestChannel;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestHandler;
Expand Down Expand Up @@ -111,7 +112,7 @@ public void testSetupRestHandlerContainsKnownBuiltin() {
ActionModule actionModule = new ActionModule(settings.getSettings(),
new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), settings.getIndexScopedSettings(),
settings.getClusterSettings(), settings.getSettingsFilter(), null, emptyList(), null,
null, usageService, null);
null, usageService, null, CompatibleVersion.CURRENT_VERSION);
actionModule.initRestHandlers(null);
// At this point the easiest way to confirm that a handler is loaded is to try to register another one on top of it and to fail
Exception e = expectThrows(IllegalArgumentException.class, () ->
Expand Down Expand Up @@ -151,7 +152,7 @@ public String getName() {
ActionModule actionModule = new ActionModule(settings.getSettings(),
new IndexNameExpressionResolver(threadPool.getThreadContext()), settings.getIndexScopedSettings(),
settings.getClusterSettings(), settings.getSettingsFilter(), threadPool, singletonList(dupsMainAction),
null, null, usageService, null);
null, null, usageService, null, CompatibleVersion.CURRENT_VERSION);
Exception e = expectThrows(IllegalArgumentException.class, () -> actionModule.initRestHandlers(null));
assertThat(e.getMessage(), startsWith("Cannot replace existing handler for [/] for method: GET"));
} finally {
Expand Down Expand Up @@ -186,7 +187,7 @@ public List<RestHandler> getRestHandlers(Settings settings, RestController restC
ActionModule actionModule = new ActionModule(settings.getSettings(),
new IndexNameExpressionResolver(threadPool.getThreadContext()), settings.getIndexScopedSettings(),
settings.getClusterSettings(), settings.getSettingsFilter(), threadPool, singletonList(registersFakeHandler),
null, null, usageService, null);
null, null, usageService, null, CompatibleVersion.CURRENT_VERSION);
actionModule.initRestHandlers(null);
// At this point the easiest way to confirm that a handler is loaded is to try to register another one on top of it and to fail
Exception e = expectThrows(IllegalArgumentException.class, () ->
Expand Down
Loading