-
Notifications
You must be signed in to change notification settings - Fork 24.7k
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
Add parsing method for ElasticsearchException.generateThrowableXContent() #22783
Conversation
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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())); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 + "]"); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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? |
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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(']'); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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())); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
…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.
* 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) ...
The output of the
ElasticsearchException.generateThrowableXContent()
method can be parsed back by theElasticsearchException.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 theElasticsearchException.fromXContent()
so that it does not throw an exception when custom metadata of type array & objects are parsed.