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

Add parsing method for ElasticsearchException.generateThrowableXContent() #22783

Merged

Conversation

tlrx
Copy link
Member

@tlrx tlrx commented Jan 25, 2017

The output of the ElasticsearchException.generateThrowableXContent() method can be parsed back by the ElasticsearchException.fromXContent() method.

This commit adds unit tests in the style of the to-and-from-xcontent tests we already have for other parsing methods. It also relax the strict parsing of the ElasticsearchException.fromXContent() so that it does not throw an exception when custom metadata of type array & objects are parsed.

Copy link
Member

@javanna javanna left a comment

Choose a reason for hiding this comment

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

thanks @tlrx I left some comments

Map<String, List<String>> metadata = new HashMap<>();
Map<String, Object> headers = new HashMap<>();

XContentParser.Token token = parser.currentToken();
Copy link
Member

Choose a reason for hiding this comment

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

shall we check what the current token is? if it is a field_name we could use a while instead of a do while? that would be a bit more readable I think.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, I didn't address this point yet.

token = parser.nextToken();
}

if ( token != null && token.isValue()) {
Copy link
Member

Choose a reason for hiding this comment

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

how can the token be null? that seems like a bug?

} else if (STACK_TRACE.equals(currentFieldName)) {
stack = parser.text();
} else {
metadata.put(currentFieldName, Collections.singletonList(parser.text()));
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we support arrays correctly here do we? metadata can be a list of values but here we create a new list for each value, and the last one wins?

Copy link
Member

Choose a reason for hiding this comment

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

oh actually we skip all arrays in this parser, so I think we support only key-pairs, but we may print out arrays too?

Copy link
Member Author

Choose a reason for hiding this comment

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

Metadata arrays are not supported at all and just skipped - we could decide to support arrays of value but this is out of scope of this pull request I think

Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should at least parse List given that we have that in some subclass? I wouldn't extend support for objects etc. here, but just for arrays of strings. that is what the addMetadata supports at the moment. I am not talking about supporting anything that may get printed when overriding metadataToXContent.

Copy link
Member Author

Choose a reason for hiding this comment

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

You're right - we must support what gets printed and it can print arrays of values. I should have added that but I forgot it on my way... Thanks for raising this.

if (CAUSED_BY.equals(currentFieldName)) {
cause = fromXContent(parser);
} else if (HEADER.equals(currentFieldName)) {
headers.putAll(parser.map());
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 I would parse headers more precisely into a Map<String, List<String>>, it shouldn't be a lot of code.

Copy link
Member Author

Choose a reason for hiding this comment

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

Make sense

// and skipped, so that the parser does not fail on unknown fields.
parser.skipChildren();
}
}else if (token == XContentParser.Token.START_ARRAY) {
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 a whitespace after the bracket please?

final Throwable throwable = exceptions.v1();

BytesReference throwableBytes = XContentHelper.toXContent((b, p) -> {
ElasticsearchException.generateThrowableXContent(b, ToXContent.EMPTY_PARAMS, throwable);
Copy link
Member

Choose a reason for hiding this comment

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

we may have done this somewhere else but I think for correct-ness we should rather pass p in instead of empty params. I would also love to call them builder and params if possible.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure

expected.addHeader("doc_id", "test");
break;
default:
throw new IllegalArgumentException("No randomized exceptions generated for type [" + type + "]");
Copy link
Member

Choose a reason for hiding this comment

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

nit: throw UnsupportedOperationException instead?

public void testThrowableToAndFromXContent() throws IOException {
final XContent xContent = randomFrom(XContentType.values()).xContent();

final Tuple<Throwable, ElasticsearchException> exceptions = randomExceptions();
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 test what happens when one calls setResources passing in multiple ids? That's the one place where we need lists to work properly.

expectedCause.addMetadata("es.col", "42");

expected = new ElasticsearchException("Elasticsearch exception [type=exception, reason=Parsing failed]", expectedCause);
expected.addHeader("doc_id", "test");
Copy link
Member

Choose a reason for hiding this comment

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

should we rather add header or metadata outside of the switch to any generated exception (randomly)? Also test lists for headers too.

assertNotNull(parsedException);
assertThat(parsedException.getHeaderKeys(), hasSize(0));
assertThat(parsedException.getMetadataKeys(), hasSize(2));
assertThat(parsedException.getMetadata("es.phase"), hasItem(phase));
Copy link
Member

Choose a reason for hiding this comment

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

the es. prefix here is a bit weird, but I don't know how to remove it without breaking bw comp. We have to take into account though that even if the info is available, no users will know how to retrieve it :)

Copy link
Member Author

Choose a reason for hiding this comment

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

They can still have a look of the content of getMetadataKeys?

Copy link
Member

Choose a reason for hiding this comment

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

right they can iterate over them, that is acceptable.

@tlrx
Copy link
Member Author

tlrx commented Jan 25, 2017

Thanks @javanna, I missed an important point concerning metadata/header list of values. I updated the code according to your comments, can you have another look please?

Copy link
Member

@javanna javanna left a comment

Choose a reason for hiding this comment

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

left a few minors, feel free to push once those are addressed.

// Arrays of objects are not supported yet and just ignored and skipped.
List<String> values = new ArrayList<>();
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
if (token.isValue() || token == XContentParser.Token.VALUE_NULL) {
Copy link
Member

Choose a reason for hiding this comment

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

out of curiosity, why supporting null?

Copy link
Member

Choose a reason for hiding this comment

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

question: what happens with fields that are not strings? do they become strings?

Copy link
Member Author

Choose a reason for hiding this comment

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

It supports null because headers/metadata/resources can contain null values and those null values will be present in the printed xcontent. So I think we have to parse them and it doesn't add extra complexity.

question: what happens with fields that are not strings? do they become strings?

parser.textOrNull() takes care to retrieve a textual representation of the value but we might lose some precision.

Copy link
Member

Choose a reason for hiding this comment

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

you mean they can contain null values because HashMap supports it or because it actually makes sense to have null values in there? I wonder if we should rather validate values and reject null.

Would it be possible to only parse strings and skip the rest for now?

Copy link
Member Author

Choose a reason for hiding this comment

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

you mean they can contain null values because HashMap supports it or because it actually makes sense to have null values in there?

Neither of the two: since we allow nulls to be added and printed I thought it would be better to also be able to parse them back.

I wonder if we should rather validate values and reject null.

Would it be possible to only parse strings and skip the rest for now?

I agree and yes, as long ass we take care of rejecting them in a follow up PR :)

Copy link
Member Author

Choose a reason for hiding this comment

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

4d43094 updates the code so that only strings are parsed.

if (stack != null) {
message.append(", ").append(STACK_TRACE).append('=').append(stack);
}
message.append(']');
Copy link
Member

Choose a reason for hiding this comment

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

I was wondering what the difference is between an exception that is created not going through parsing, and an exception that gets parsed. Wondering about the message specifically.

assertNotNull(parsedException);

ElasticsearchException expected = exceptions.v2();
while (expected != null) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: here I would maybe use do while just to make absolutely sure that the first round always runs.

throw new UnsupportedOperationException("No randomized exceptions generated for type [" + type + "]");
}

if (actual instanceof ElasticsearchException) {
Copy link
Member

Choose a reason for hiding this comment

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

you can probably cast it here and reuse it later

if (randomBoolean()) {
Map<String, List<String>> randomHeaders = new HashMap<>();

int nbHeaders = randomIntBetween(1, 5);
Copy link
Member

Choose a reason for hiding this comment

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

super minor nit: in general, when creating maps and lists and knowing the size you could pass it in.

int type = randomIntBetween(0, 5);
switch (type) {
case 0:
actual = new ClusterBlockException(singleton(DiscoverySettings.NO_MASTER_BLOCK_WRITES));
Copy link
Member

Choose a reason for hiding this comment

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

oh I see the trouble that I caused you, because not all of them are actually ElasticsearchException. Good test though, much more coverage now!

if (CAUSED_BY.equals(currentFieldName)) {
cause = fromXContent(parser);
} else if (HEADER.equals(currentFieldName)) {
Map<String, Object> headerMap = parser.map();
Copy link
Member

Choose a reason for hiding this comment

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

you are still casting here and eventually calling toString on Objects. What I meant is manually parse these headers instead and make the parsing code a little more accurate. That also won't require to go through the map once again once parsed.

token = parser.nextToken();
}

if (token != null && token.isValue()) {
Copy link
Member

Choose a reason for hiding this comment

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

Same question as before: how can the token be null? that seems like a bug?

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, I removed that.

Map<String, List<String>> metadata = new HashMap<>();
Map<String, List<String>> headers = new HashMap<>();

XContentParser.Token token = parser.currentToken();
Copy link
Member

Choose a reason for hiding this comment

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

same as before: shall we check what the current token is? if it is a field_name we could use a while instead of a do while? that would be a bit more readable I think.

Copy link
Member Author

Choose a reason for hiding this comment

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

I changed this part a bit. It's tricky to use a while ((token = parser.nextToken()) == ...) like we do in other parsers because we mustn't parse the next token while entering the loop but we still need to do it while leaving the loop (so that the method left the parser in a coherent state, ie right after the last known field has been parsed).

I changed this to use a for loop, hoping it's easier to read now.

} else if (STACK_TRACE.equals(currentFieldName)) {
stack = parser.text();
} else {
metadata.put(currentFieldName, Collections.singletonList(parser.text()));
Copy link
Member

Choose a reason for hiding this comment

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

shall we also skip non strings here?

Copy link
Member Author

Choose a reason for hiding this comment

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

If we skip strings here, that would mean that some non-string metadata (like bytes_wanted & bytes_limit from CircuitBreakingException or line & col from ParsingException) won't be parsed back... which is sad I think, but maybe more coherent.

Copy link
Member

Choose a reason for hiding this comment

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

yes I know, I value consistency here, let's not support things only in specific cases. we will parse those back when/if we will support proper numbers.

Copy link
Member Author

Choose a reason for hiding this comment

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

Let's do that then

expected.setResources(resourceType, resourceIds);
}
}
}
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 test the case where we have numbers which get ignored?

Copy link
Member Author

Choose a reason for hiding this comment

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

I added a dedicated test for this.

Copy link
Member

Choose a reason for hiding this comment

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

wonderful, thanks

if (token == XContentParser.Token.VALUE_STRING) {
values.add(parser.text());
} else {
parser.skipChildren();
Copy link
Member

Choose a reason for hiding this comment

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

is this needed here? I think it does something only when the current token is start array or start object.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, and in other cases it's a noop. But here you could have arrays of objects (see ExportException)

@tlrx tlrx merged commit be96278 into elastic:master Jan 26, 2017
tlrx added a commit that referenced this pull request Jan 26, 2017
…nt() (#22783)

The output of the ElasticsearchException.generateThrowableXContent() method can be parsed back by the ElasticsearchException.fromXContent() method.

This commit adds unit tests in the style of the to-and-from-xcontent tests we already have for other parsing methods. It also relax the strict parsing of the ElasticsearchException.fromXContent() so that it does not throw an exception when custom metadata and headers are parsed, as long as they are either strings or arrays of strings. Every other type is ignored at parsing time.
@tlrx
Copy link
Member Author

tlrx commented Jan 26, 2017

Thanks a lot @javanna - this wasn't fun to review at all.

5.x: e7a6cd4

@tlrx tlrx removed the review label Jan 26, 2017
@tlrx tlrx deleted the add-parsing-tests-for-generate-throwable-to-xcontent branch January 26, 2017 14:37
jasontedor added a commit to jasontedor/elasticsearch that referenced this pull request Jan 26, 2017
* master: (47 commits)
  Remove non needed import
  use expectThrows instead of manually testing exception
  Fix checkstyle and a test
  Update after review
  Read ec2 discovery address from aws instance tags
  Invalidate cached query results if query timed out (elastic#22807)
  Add remaining generated painless API
  Generate reference links for painless API (elastic#22775)
  [TEST] Fix ElasticsearchExceptionTests
  Add parsing method for ElasticsearchException.generateThrowableXContent() (elastic#22783)
  Improve connection closing in `RemoteClusterConnection` (elastic#22804)
  Docs: Cluster allocation explain should be on one page
  Remove DFS_QUERY_AND_FETCH as a search type (elastic#22787)
  Add repository-url module and move URLRepository (elastic#22752)
  fix date-processor to a new default year for every new pipeline execution. (elastic#22601)
  Add tests for top_hits aggregation (elastic#22754)
  [TEST] Added this for 93a28b0 submitted via elastic#22772
  Fix typo in comment in OsProbe.java
  Add new ruby search library to community clients doc (elastic#22765)
  RangeQuery WITHIN case now normalises query (elastic#22431)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants