-
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
Changes from 5 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 |
---|---|---|
|
@@ -36,6 +36,7 @@ | |
import org.elasticsearch.transport.TcpTransport; | ||
|
||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
|
@@ -50,7 +51,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 +391,125 @@ 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 { | ||
XContentParser.Token token = parser.currentToken(); | ||
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, List<String>> headers = new HashMap<>(); | ||
|
||
for (; token == XContentParser.Token.FIELD_NAME; token = parser.nextToken()) { | ||
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())); | ||
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. shall we also skip non strings here? 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. If we skip strings here, that would mean that some non-string metadata (like 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. 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 commentThe reason will be displayed to describe this comment to others. Learn more. Let's do that then |
||
} | ||
} else if (token == XContentParser.Token.START_OBJECT) { | ||
if (CAUSED_BY.equals(currentFieldName)) { | ||
cause = fromXContent(parser); | ||
} else if (HEADER.equals(currentFieldName)) { | ||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { | ||
if (token == XContentParser.Token.FIELD_NAME) { | ||
currentFieldName = parser.currentName(); | ||
} else { | ||
List<String> values = headers.getOrDefault(currentFieldName, new ArrayList<>()); | ||
if (token == XContentParser.Token.VALUE_STRING) { | ||
values.add(parser.text()); | ||
} else if (token == XContentParser.Token.START_ARRAY) { | ||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { | ||
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 commentThe 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 commentThe 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) |
||
} | ||
} | ||
} else if (token == XContentParser.Token.START_OBJECT) { | ||
parser.skipChildren(); | ||
} | ||
headers.put(currentFieldName, values); | ||
} | ||
} | ||
} else { | ||
// Any additional metadata object added by the metadataToXContent method is ignored | ||
// and skipped, so that the parser does not fail on unknown fields. The parser only | ||
// support metadata key-pairs and metadata arrays of values. | ||
parser.skipChildren(); | ||
} | ||
} else if (token == XContentParser.Token.START_ARRAY) { | ||
// Parse the array and add each item to the corresponding list of metadata. | ||
// 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 == XContentParser.Token.VALUE_STRING) { | ||
values.add(parser.text()); | ||
} else { | ||
parser.skipChildren(); | ||
} | ||
} | ||
if (values.size() > 0) { | ||
if (metadata.containsKey(currentFieldName)) { | ||
values.addAll(metadata.get(currentFieldName)); | ||
} | ||
metadata.put(currentFieldName, values); | ||
} | ||
} | ||
} | ||
|
||
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(']'); | ||
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 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 :( 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, List<String>> header : headers.entrySet()) { | ||
e.addHeader(header.getKey(), 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 +568,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 | ||
*/ | ||
|
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.