-
Notifications
You must be signed in to change notification settings - Fork 25.6k
Add parsing method for ElasticsearchException.generateThrowableXContent() #22783
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
Changes from 2 commits
7c079f1
5d32a94
5ac4801
0ac62a8
4d43094
320023c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,7 +50,6 @@ | |
| import static java.util.Collections.unmodifiableMap; | ||
| import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_UUID_NA_VALUE; | ||
| import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; | ||
| import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownField; | ||
|
|
||
| /** | ||
| * A base class for all elasticsearch exceptions. | ||
|
|
@@ -391,12 +390,93 @@ private static void headerToXContent(XContentBuilder builder, String key, List<S | |
| protected void metadataToXContent(XContentBuilder builder, Params params) throws IOException { | ||
| } | ||
|
|
||
| /** | ||
| * Generate a {@link ElasticsearchException} from a {@link XContentParser}. This does not | ||
| * return the original exception type (ie NodeClosedException for example) but just wraps | ||
| * the type, the reason and the cause of the exception. It also recursively parses the | ||
| * tree structure of the cause, returning it as a tree structure of {@link ElasticsearchException} | ||
| * instances. | ||
| */ | ||
| public static ElasticsearchException fromXContent(XContentParser parser) throws IOException { | ||
| XContentParser.Token token = parser.nextToken(); | ||
| ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation); | ||
|
|
||
| return innerFromXContent(parser); | ||
| } | ||
|
|
||
| private static ElasticsearchException innerFromXContent(XContentParser parser) throws IOException { | ||
| String type = null, reason = null, stack = null; | ||
| ElasticsearchException cause = null; | ||
| Map<String, List<String>> metadata = new HashMap<>(); | ||
| Map<String, Object> headers = new HashMap<>(); | ||
|
|
||
| XContentParser.Token token = parser.currentToken(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed this part a bit. It's tricky to use a I changed this to use a for loop, hoping it's easier to read now. |
||
| String currentFieldName = parser.currentName(); | ||
| do { | ||
| if (token == XContentParser.Token.FIELD_NAME) { | ||
| currentFieldName = parser.currentName(); | ||
| token = parser.nextToken(); | ||
| } | ||
|
|
||
| if ( token != null && token.isValue()) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how can the token be null? that seems like a bug? |
||
| if (TYPE.equals(currentFieldName)) { | ||
| type = parser.text(); | ||
| } else if (REASON.equals(currentFieldName)) { | ||
| reason = parser.text(); | ||
| } else if (STACK_TRACE.equals(currentFieldName)) { | ||
| stack = parser.text(); | ||
| } else { | ||
| metadata.put(currentFieldName, Collections.singletonList(parser.text())); | ||
|
||
| } | ||
| } else if (token == XContentParser.Token.START_OBJECT) { | ||
| if (CAUSED_BY.equals(currentFieldName)) { | ||
| cause = fromXContent(parser); | ||
| } else if (HEADER.equals(currentFieldName)) { | ||
| headers.putAll(parser.map()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I would parse headers more precisely into a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make sense |
||
| } else { | ||
| // Additional metadata added by the metadataToXContent method is ignored | ||
| // and skipped, so that the parser does not fail on unknown fields. | ||
| parser.skipChildren(); | ||
| } | ||
| }else if (token == XContentParser.Token.START_ARRAY) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you add a whitespace after the bracket please? |
||
| // Additional metadata added by the metadataToXContent method is ignored | ||
| // and skipped, so that the parser does not fail on unknown fields. | ||
|
||
| parser.skipChildren(); | ||
| } | ||
| } while ((token = parser.nextToken()) == XContentParser.Token.FIELD_NAME); | ||
|
|
||
| StringBuilder message = new StringBuilder("Elasticsearch exception ["); | ||
| message.append(TYPE).append('=').append(type).append(", "); | ||
| message.append(REASON).append('=').append(reason); | ||
| if (stack != null) { | ||
| message.append(", ").append(STACK_TRACE).append('=').append(stack); | ||
| } | ||
| message.append(']'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we compare messages from a normal ElasticsearchException with and without cause, with ones obtained from a parsed one? I wonder specifically if the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure to understand what you mean, but with or without cause you'll end up with something like Elasticsearch exception [type=foo, reason=bar], and with few cycles of toXContent/fromXContent it will end up with Another bad point is the current "No ElasticsearchException found" in the generateFailureXContent() method which can obfuscate any non ElasticsearchException :(
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| ElasticsearchException e = new ElasticsearchException(message.toString(), cause); | ||
|
|
||
| for (Map.Entry<String, List<String>> entry : metadata.entrySet()) { | ||
| //subclasses can print out additional metadata through the metadataToXContent method. Simple key-value pairs will be | ||
| //parsed back and become part of this metadata set, while objects and arrays are not supported when parsing back. | ||
| //Those key-value pairs become part of the metadata set and inherit the "es." prefix as that is currently required | ||
| //by addMetadata. The prefix will get stripped out when printing metadata out so it will be effectively invisible. | ||
| //TODO move subclasses that print out simple metadata to using addMetadata directly and support also numbers and booleans. | ||
| //TODO rename metadataToXContent and have only SearchPhaseExecutionException use it, which prints out complex objects | ||
| e.addMetadata("es." + entry.getKey(), entry.getValue()); | ||
| } | ||
| for (Map.Entry<String, Object> header : headers.entrySet()) { | ||
| e.addHeader(header.getKey(), String.valueOf(header.getValue())); | ||
| } | ||
| return e; | ||
| } | ||
|
|
||
| /** | ||
| * Static toXContent helper method that renders {@link org.elasticsearch.ElasticsearchException} or {@link Throwable} instances | ||
| * as XContent, delegating the rendering to {@link #toXContent(XContentBuilder, Params)} | ||
| * or {@link #innerToXContent(XContentBuilder, Params, Throwable, String, String, Map, Map, Throwable)}. | ||
| * | ||
| * This method is usually used when the {@link Throwable} is rendered as a part of another XContent object. | ||
| * This method is usually used when the {@link Throwable} is rendered as a part of another XContent object, and its result can | ||
| * be parsed back using the {@link #fromXContent(XContentParser)} method. | ||
| */ | ||
| public static void generateThrowableXContent(XContentBuilder builder, Params params, Throwable t) throws IOException { | ||
| t = ExceptionsHelper.unwrapCause(t); | ||
|
|
@@ -455,71 +535,6 @@ public static void generateFailureXContent(XContentBuilder builder, Params param | |
| builder.endObject(); | ||
| } | ||
|
|
||
| /** | ||
| * Generate a {@link ElasticsearchException} from a {@link XContentParser}. This does not | ||
| * return the original exception type (ie NodeClosedException for example) but just wraps | ||
| * the type, the reason and the cause of the exception. It also recursively parses the | ||
| * tree structure of the cause, returning it as a tree structure of {@link ElasticsearchException} | ||
| * instances. | ||
| */ | ||
| public static ElasticsearchException fromXContent(XContentParser parser) throws IOException { | ||
| XContentParser.Token token = parser.nextToken(); | ||
| ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation); | ||
|
|
||
| String type = null, reason = null, stack = null; | ||
| ElasticsearchException cause = null; | ||
| Map<String, List<String>> metadata = new HashMap<>(); | ||
| Map<String, Object> headers = new HashMap<>(); | ||
|
|
||
| do { | ||
| String currentFieldName = parser.currentName(); | ||
| token = parser.nextToken(); | ||
| if (token.isValue()) { | ||
| if (TYPE.equals(currentFieldName)) { | ||
| type = parser.text(); | ||
| } else if (REASON.equals(currentFieldName)) { | ||
| reason = parser.text(); | ||
| } else if (STACK_TRACE.equals(currentFieldName)) { | ||
| stack = parser.text(); | ||
| } else { | ||
| metadata.put(currentFieldName, Collections.singletonList(parser.text())); | ||
| } | ||
| } else if (token == XContentParser.Token.START_OBJECT) { | ||
| if (CAUSED_BY.equals(currentFieldName)) { | ||
| cause = fromXContent(parser); | ||
| } else if (HEADER.equals(currentFieldName)) { | ||
| headers.putAll(parser.map()); | ||
| } else { | ||
| throwUnknownField(currentFieldName, parser.getTokenLocation()); | ||
| } | ||
| } | ||
| } while ((token = parser.nextToken()) == XContentParser.Token.FIELD_NAME); | ||
|
|
||
| StringBuilder message = new StringBuilder("Elasticsearch exception ["); | ||
| message.append(TYPE).append('=').append(type).append(", "); | ||
| message.append(REASON).append('=').append(reason); | ||
| if (stack != null) { | ||
| message.append(", ").append(STACK_TRACE).append('=').append(stack); | ||
| } | ||
| message.append(']'); | ||
|
|
||
| ElasticsearchException e = new ElasticsearchException(message.toString(), cause); | ||
|
|
||
| for (Map.Entry<String, List<String>> entry : metadata.entrySet()) { | ||
| //subclasses can print out additional metadata through the metadataToXContent method. Simple key-value pairs will be | ||
| //parsed back and become part of this metadata set, while objects and arrays are not supported when parsing back. | ||
| //Those key-value pairs become part of the metadata set and inherit the "es." prefix as that is currently required | ||
| //by addMetadata. The prefix will get stripped out when printing metadata out so it will be effectively invisible. | ||
| //TODO move subclasses that print out simple metadata to using addMetadata directly and support also numbers and booleans. | ||
| //TODO rename metadataToXContent and have only SearchPhaseExecutionException use it, which prints out complex objects | ||
| e.addMetadata("es." + entry.getKey(), entry.getValue()); | ||
| } | ||
| for (Map.Entry<String, Object> header : headers.entrySet()) { | ||
| e.addHeader(header.getKey(), String.valueOf(header.getValue())); | ||
| } | ||
| return e; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the root cause of this exception or multiple if different shards caused different exceptions | ||
| */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,8 +27,10 @@ | |
| import org.elasticsearch.cluster.block.ClusterBlockException; | ||
| import org.elasticsearch.common.ParsingException; | ||
| import org.elasticsearch.common.Strings; | ||
| import org.elasticsearch.common.breaker.CircuitBreakingException; | ||
| import org.elasticsearch.common.bytes.BytesArray; | ||
| import org.elasticsearch.common.bytes.BytesReference; | ||
| import org.elasticsearch.common.collect.Tuple; | ||
| import org.elasticsearch.common.xcontent.ToXContent; | ||
| import org.elasticsearch.common.xcontent.XContent; | ||
| import org.elasticsearch.common.xcontent.XContentBuilder; | ||
|
|
@@ -516,6 +518,41 @@ public void testFromXContentWithHeadersAndMetadata() throws IOException { | |
| assertThat(cause.getMetadata("es.index_uuid"), hasItem("_na_")); | ||
| } | ||
|
|
||
| public void testThrowableToAndFromXContent() throws IOException { | ||
| final XContent xContent = randomFrom(XContentType.values()).xContent(); | ||
|
|
||
| final Tuple<Throwable, ElasticsearchException> exceptions = randomExceptions(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we test what happens when one calls |
||
| final Throwable throwable = exceptions.v1(); | ||
|
|
||
| BytesReference throwableBytes = XContentHelper.toXContent((b, p) -> { | ||
| ElasticsearchException.generateThrowableXContent(b, ToXContent.EMPTY_PARAMS, throwable); | ||
|
||
| return b; | ||
| }, xContent.type(), randomBoolean()); | ||
|
|
||
| ElasticsearchException parsedException; | ||
| try (XContentParser parser = createParser(xContent, throwableBytes)) { | ||
| assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); | ||
| parsedException = ElasticsearchException.fromXContent(parser); | ||
| assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); | ||
| assertNull(parser.nextToken()); | ||
| } | ||
| assertNotNull(parsedException); | ||
|
|
||
| ElasticsearchException expected = exceptions.v2(); | ||
| while (expected != null) { | ||
|
||
| assertEquals(expected.getMessage(), parsedException.getMessage()); | ||
| assertEquals(expected.getHeaders(), parsedException.getHeaders()); | ||
| assertEquals(expected.getMetadata(), parsedException.getMetadata()); | ||
|
|
||
| if (expected.getCause() != null) { | ||
| expected = (ElasticsearchException) expected.getCause(); | ||
| parsedException = (ElasticsearchException) parsedException.getCause(); | ||
| } else { | ||
| assertNull(parsedException.getCause()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Builds a {@link ToXContent} using a JSON XContentBuilder and check the resulting string with the given {@link Matcher}. | ||
| * | ||
|
|
@@ -533,4 +570,64 @@ private static void assertExceptionAsJson(Exception e, String expectedJson) thro | |
| return builder; | ||
| }, expectedJson); | ||
| } | ||
|
|
||
| private static Tuple<Throwable, ElasticsearchException> randomExceptions() { | ||
| Throwable actual; | ||
| ElasticsearchException expected; | ||
|
|
||
| int type = randomIntBetween(0, 5); | ||
| switch (type) { | ||
| case 0: | ||
| actual = new ClusterBlockException(singleton(DiscoverySettings.NO_MASTER_BLOCK_WRITES)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! |
||
| expected = new ElasticsearchException("Elasticsearch exception [type=cluster_block_exception, " + | ||
| "reason=blocked by: [SERVICE_UNAVAILABLE/2/no master];]"); | ||
| break; | ||
| case 1: | ||
| actual = new CircuitBreakingException("Data too large", 123, 456); | ||
| expected = new ElasticsearchException("Elasticsearch exception [type=circuit_breaking_exception, reason=Data too large]"); | ||
| expected.addMetadata("es.bytes_wanted", "123"); | ||
| expected.addMetadata("es.bytes_limit", "456"); | ||
| break; | ||
| case 2: | ||
| actual = new SearchParseException(new TestSearchContext(null), "Parse failure", new XContentLocation(12, 98)); | ||
| expected = new ElasticsearchException("Elasticsearch exception [type=search_parse_exception, reason=Parse failure]"); | ||
| expected.addMetadata("es.line", "12"); | ||
| expected.addMetadata("es.col", "98"); | ||
| break; | ||
| case 3: | ||
| actual = new IllegalArgumentException("Closed resource", new RuntimeException("Resource")); | ||
| expected = new ElasticsearchException("Elasticsearch exception [type=illegal_argument_exception, reason=Closed resource]", | ||
| new ElasticsearchException("Elasticsearch exception [type=runtime_exception, reason=Resource]")); | ||
| break; | ||
| case 4: | ||
| actual = new SearchPhaseExecutionException("search", "all shards failed", | ||
| new ShardSearchFailure[]{ | ||
| new ShardSearchFailure(new ParsingException(1, 2, "foobar", null), | ||
| new SearchShardTarget("node_1", new Index("foo", "_na_"), 1)) | ||
| }); | ||
| expected = new ElasticsearchException("Elasticsearch exception [type=search_phase_execution_exception, " + | ||
| "reason=all shards failed]"); | ||
| expected.addMetadata("es.grouped", "true"); | ||
| expected.addMetadata("es.phase", "search"); | ||
| break; | ||
| case 5: | ||
| actual = new ElasticsearchException("Parsing failed", | ||
| new ParsingException(9, 42, "Wrong state", | ||
| new NullPointerException("Unexpected null value"))); | ||
| ((ElasticsearchException) actual).addHeader("doc_id", "test"); | ||
|
|
||
| ElasticsearchException expectedCause = new ElasticsearchException("Elasticsearch exception [type=parsing_exception, " + | ||
| "reason=Wrong state]", new ElasticsearchException("Elasticsearch exception [type=null_pointer_exception, " + | ||
| "reason=Unexpected null value]")); | ||
| expectedCause.addMetadata("es.line", "9"); | ||
| expectedCause.addMetadata("es.col", "42"); | ||
|
|
||
| expected = new ElasticsearchException("Elasticsearch exception [type=exception, reason=Parsing failed]", expectedCause); | ||
| expected.addHeader("doc_id", "test"); | ||
|
||
| break; | ||
| default: | ||
| throw new IllegalArgumentException("No randomized exceptions generated for type [" + type + "]"); | ||
|
||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we test the case where we have numbers which get ignored?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a dedicated test for this.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wonderful, thanks |
||
| return new Tuple<>(actual, expected); | ||
| } | ||
| } | ||
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.