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 version of typed Index And Get API #53228

Merged
merged 28 commits into from
Mar 25, 2020

Conversation

pgomulka
Copy link
Contributor

@pgomulka pgomulka commented Mar 6, 2020

Providing a REST infrastructure to handle compatible APIs. Rest APIs that were removed in 8 will be available only if an HTTP request had a compatible header. At the moment is is expecting an Accept header to contain application/vnd.elasticsearch+json;compatible-with=7. This might be changed though when #52370 is resolved.
The REST changes contain

  • enriching RestRequest with a parameter Compatible-With. The value is taken from Accept header. See RestRequest creation. Used when deciding if a request to a compatible handler can be dispatched. See RestController.
    //TODO this at the moment can be simplified to only use a value from a header. However in rest layer we often have only access to Params, so headers won't be accessible when implementing some compatible code

  • enriching XContentBuilder with a compatible version value. See AbstractRestChannel. Used when shape of a response is changed. see DocWriteResponse and GetResult

  • MediaType parsing change - see XContentType. because we expect a different media type now application/vnd.elasticsearch+json;compatible-with=7 (when previously application/json or similar)

  • testing changes - compat tests will have a compatible header set. see AbstractCompatRestTest.java

  • Compatible rest handlers - based on RestIndexAction and RestGetAction. See modules/rest-compatibility. These extend already existing handlers and register them in a RestCompatPlugin under a typed paths. They will only be accessible when a compatible header is present (use of compatibilityRequired method from RestHandler interface)

Without this PR 283 tests from 7.x are failing. - rest api spec tests only
with this PR 228 tests are passing - with almost all tests from index/* package passed (missing 2 tests that require change for include_type_param, to be added in a new PR)

relates #51816

1226 tests, 227 failures, 16ignored
23 / 2 failled in index/*
failing
CompatRestIT. test {yaml=index/70_mix_typeless_typeful/Index call that
introduces new field mappings}
CompatRestIT. test {yaml=index/70_mix_typeless_typeful/Index with
typeless API on an index that has types}
@pgomulka pgomulka added the :Core/Infra/Core Core issues without another label label Mar 6, 2020
@pgomulka pgomulka self-assigned this Mar 6, 2020
@elasticmachine
Copy link
Collaborator

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

@pgomulka pgomulka changed the base branch from master to compat_rest_api March 6, 2020 15:21
@pgomulka pgomulka added the WIP label Mar 6, 2020
@pgomulka pgomulka changed the base branch from compat_rest_api to master March 9, 2020 16:34
@pgomulka pgomulka changed the base branch from master to compat_rest_api March 9, 2020 16:39
@@ -86,4 +100,30 @@ protected RestStatus getStatus(final GetResponse response) {
});
}

public static class CompatibleRestGetAction extends RestGetAction {
Copy link
Contributor

Choose a reason for hiding this comment

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

Until we defined a better approach, can you start adding in something like this

assert Version.CURRENT.major == 8 : "REST API compatilbity for version 7 is only supported on version 8";```

import java.util.function.Consumer;
import java.util.function.UnaryOperator;

public class CompatibleHandlers {
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 add a TODO in here to ensure we get some buy in on #52370 before we merge the header format.

@pgomulka
Copy link
Contributor Author

ok to test

@pgomulka pgomulka added :Core/Infra/REST API REST infrastructure and utilities and removed WIP :Core/Infra/Core Core issues without another label labels Mar 11, 2020
FakeRestRequest fakeRestRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY)
.withContent(new BytesArray(content), RestRequest.parseContentType(contentTypeHeader)).withPath("/foo")
.withHeaders(Map.of("Content-Type", contentTypeHeader, "Accept", contentTypeHeader))
.build();
Copy link
Contributor

Choose a reason for hiding this comment

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

Also, what happens if you set the Content-Type to application/json, but Accept to application/vnd.elasticsearch+json;compatible-with=7 ?

I wonder if we need more testing for these edge cases ?(either unit or integration)

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 question. we can be more specific for response generation with Accept and request parsing with Content-Type.
But on the other hand I think it will just complicate matters too much.
I would prefer to assume (and assert in code) that these are the same

Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above (I added a "parking lot" section on #51816 so we don't forget.)

Copy link
Contributor

@jtibshirani jtibshirani left a comment

Choose a reason for hiding this comment

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

I added a couple comments from the types deprecation side. One high-level comment: we should ensure there's test coverage for all the deprecation messages we're adding back. In 7.x we had dedicated unit tests in the various actions -- here's an example for RestIndexAction.


public class CompatibleHandlers {

public static Consumer<RestRequest> consumeParameterType(DeprecationLogger deprecationLogger) {
Copy link
Contributor

@jtibshirani jtibshirani Mar 11, 2020

Choose a reason for hiding this comment

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

I think renaming this to consumeTypeParameter would make its purpose more clear. Also, could we just have a method void consumeParameterType(DeprecationLogger deprecationLogger, RestRequest request) instead of using a Consumer? Let me know if I'm missing something around why consumers are necessary, to me they make the logic less straightforward.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agree with both - method rename and making this a method.
Originally the reason for this being a consumer was because I was trying to fit in into the previous style of registering handlers. Extending the RestController.registerHandler with additional handlerWrappers. This is no longer needed after the recent refactoring made by Jay

private static final DeprecationLogger deprecationLogger = new DeprecationLogger(LogManager.getLogger(RestGetAction.class));
private static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Specifying types in "
+ "document get requests is deprecated, use the /{index}/_doc/{id} endpoint instead.";
private static final Consumer<RestRequest> DEPRECATION_WARNING = r -> deprecationLogger.deprecatedAndMaybeLog(
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we avoid introducing a Consumer here and just call deprecationLogger.deprecatedAndMaybeLog(...) directly inside prepareRequest?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure, that similarly to previous use of consumer was influenced by previous style of registering handlers.
a direct call is better here.

String TYPES_DEPRECATION_MESSAGE = "[types removal] Using type as a path parameter is deprecated.";

return r -> {
deprecationLogger.deprecatedAndMaybeLog("create_index_with_types", TYPES_DEPRECATION_MESSAGE);
Copy link
Contributor

@jtibshirani jtibshirani Mar 11, 2020

Choose a reason for hiding this comment

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

If I'm understanding correctly, it seems like we'll log two deprecation messages per request, both the endpoint-specific one like "Specifying types in document get requests is deprecated..." and this more general one "Using type as a path parameter is deprecated". I think we could just stick with the endpoint-specific one to avoid warning twice about the same issue.

As a side note, this general deprecation message is also logged under the key "create_index_with_types", I think we'd want a more precise name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agree - let's stick with just per endpoint warning.
WIth that I feel like we can even get rid of CompatibleHandlers.consumeParameterType - it would only consume a type param - and the whole utility class.
I was worried that it would end up a bag of all the helpers for compatibility. It might generate a little bit of duplication, but there is no need for it now.

Do you mean more precise name for a deprecation key? Like to be the same for Get and Index and other handlers?

@@ -276,6 +280,9 @@ public void writeTo(StreamOutput out) throws IOException {
@Override
public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
if (builder.getCompatibleMajorVersion() == Version.V_7_0_0.major) {
builder.field(TYPE_FIELD_NAME, SINGLE_MAPPING_TYPE);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you can just do the following, I've never had to wrap a string inside a Text object:

builder.field(TYPE_FIELD_NAME, MapperService.SINGLE_MAPPING_NAME);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will do

@@ -107,4 +107,6 @@ public void testToXContentDoesntIncludeForcedRefreshUnlessForced() throws IOExce
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Small comment, would be good to revert this.

Copy link
Member

@rjernst rjernst left a comment

Choose a reason for hiding this comment

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

This is not an exhaustive review, but I have a few minor comments.

) {
if (Version.CURRENT.major == 8) {
return List.of(
new RestGetActionV7(),
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't we never have more than one major versions worth of compat in a single version? If that is the case, why do we need to version the class names?

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 is a narrow window where 2 versions can be present.
Let's say we released version 8 (or a feature freeze) and we are now already developing ES9 and there will be features compatible with v8.
We won't be able to remove the v7 compat features instantly so for some time there will be both.

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 +1 on using the version in the class for this reason, and for the obviousness they provide. While (arguably) ugly, these classes are special in that they are not intended to be maintained, rather they are intended to be deleted after a some point in time. Having the version in the name makes is more obvious and helps to prevent accidental modifications (like from a simple class search) and makes it really obvious if these things exist for too long.

classname 'org.elasticsearch.rest.compat.RestCompatPlugin'
}

integTest.enabled = false
Copy link
Member

Choose a reason for hiding this comment

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

I see tests in this PR; why are they disabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

they should be enabled. fixed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

actually there are no integTests, but there are unit tests so integTests.enabled = false tests.enabled=true

@@ -276,6 +278,9 @@ public void writeTo(StreamOutput out) throws IOException {
@Override
public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
if (builder.getCompatibleMajorVersion() == Version.V_7_0_0.major) {
Copy link
Member

Choose a reason for hiding this comment

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

If we are just looking at major version number, using the Version constant just to extract "7" seems overkill and less readable than just using the literal 7.

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 think this will help the removal of compatible code. We have not decided on what mechanism to use in order to help finding this code.
The plan at the moment is to put as much code v7-compatible into module/rest-compatibility
Using Version.V_7_0_0.major would make sure that if the version is removed after major upgrade it will also highlight the places where v7-compatible code has to be removed

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, the removal of the of the Version.V_7_0_0 constant in the code (for v9) will cause this to fail compilation. Which is likely the correct the behavior. It does indeed make major version bumps harder (one more thing that breaks), but for now I think this is the best we can do. It feels like there is something that can be done to ease that upgrade pain, but that is outside the scope of this PR.

@@ -340,6 +349,11 @@ private void tryAllHandlers(final RestRequest request, final RestChannel channel
handleBadRequest(uri, requestMethod, channel);
}

public static boolean isV7Compatible(ToXContent.Params params) {
Copy link
Member

Choose a reason for hiding this comment

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

Why is this V7 specific? Aren't the compatible params version agnostic?

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, fixed

@jakelandis
Copy link
Contributor

jakelandis commented Mar 19, 2020

@pgomulka - looking good. I have question about error handling and the combinations of Accept and Content-Type headers.

Indexing with types and compatibility mode requires both Accept and Content-Type headers. This is how it is implemented and I believe is the correct behavior. We don't want to open the door (at least not now) for mixing normal requests with compatible responses (or vice-versa).

For example this works just fine:

curl -v -H "Accept: application/vnd.elasticsearch+json;compatible-with=7" -H "Content-Type: application/vnd.elasticsearch+json;compatible-with=7" -X PUT localhost:9200/foo/blah/1 -d '{ "b" : true }'

However, I think we could be more helpful for when it appears compatility mode is request. For example the following (correctly) doesn't work :

curl -v  -H "Content-Type: application/vnd.elasticsearch+json;compatible-with=7" -X PUT localhost:9200/foo/blah/1 -d '{ "b" : true }'

curl -v -H "Accept: application/json" -H "Content-Type: application/vnd.elasticsearch+json;compatible-with=7" -X PUT localhost:9200/foo/blah/1 -d '{ "b" : true }'

curl -v -H "Accept: application/vnd.elasticsearch+json;compatible-with=7"  -X PUT localhost:9200/foo/blah/1 -d '{ "b" : true }'

I think the above cases are reasonable attempts to trigger compatibility mode, but for (arguably pedantic) reasons don't. I think these should error in a meaningful way, maybe a 406 with a description stating that you need to set both the Accept and Content-Type header to trigger compatibility when a there is request body ?

I think the logic would be be if no body required on the request then only Accept is needed (we can ignore Content-Type), if a body is needed, then both Accept and Content-Type must be sent and equal to each other's compatible request, if a body is needed but only 1 of the Accept or Content-Type header is sent (or both are sent and they mismatch), then error with a 406 and meaningful error message.

To put it another way if the Content-Type is needed, then so is the Accept header, and they need to match w.r.t compatibility, but the Accept header by itself is OK assuming there is no body sent in the request.

Also we may want to surface a warning in the logs around poorly requested no-body requests. For example (the Content-Type is being ignored (I don't think the invalid version matters))

 curl  -v -H "Accept:: application/vnd.elasticsearch+json;compatible-with=7" -H "Content-Type: application/vnd.elasticsearch+json;compatible-with=99"  -X GET localhost:9200/foo/mytype/1

Also, for a valid request with correct header combinations, but invalid version, I believe that too should be an 406 error. For example:

 curl  -v -H "Accept:: application/vnd.elasticsearch+json;compatible-with=99" -X GET localhost:9200/foo/mytype/1

Thoughts ?

Also, I am fine if this happens in a follow up PR.

@pgomulka
Copy link
Contributor Author

@jakelandis totally agree with all points raised. I wonder if we should not do this in a separate PR. Will raise it shortly.

@pgomulka
Copy link
Contributor Author

ok to test

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.

LGTM. Let's get this in the feature branch as the foundation for other compatibility changes.

Copy link
Contributor

@jtibshirani jtibshirani left a comment

Choose a reason for hiding this comment

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

I didn't do an exhaustive review so I won't give an explicit approval, but all my earlier comments have been addressed. Thanks!

@pgomulka pgomulka mentioned this pull request Mar 25, 2020
66 tasks
@pgomulka pgomulka merged commit 878ed12 into elastic:compat_rest_api Mar 25, 2020
pgomulka added a commit that referenced this pull request Apr 20, 2020
Adding validation of Accept And Content-Type headers. The idea is to verify combinations of presence and values of both headers depending if a request has a content, headers are versioned (type/vnd.elasticsearch+subtype;compatible-with) .
It also changes the way media type is parsed (previously always assuming application as a type i.e application/json) It should expect different types like text - used in SQL
Not adding a compatible headers for _cat api. These in order to return a default text do not expect Accept header (Content-type is not needed as content is not present)

See #53228 (comment) for more context
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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants