Skip to content

Commit f4e629c

Browse files
committed
Handle all exceptions thrown during a resolution job. Allow the Java stack trace and all hits and queries up to the point of failure to be included in the response when a resolution job fails. Set the HTTP response status code to 500 when a resolution job fails.
1 parent 431e2d8 commit f4e629c

File tree

2 files changed

+69
-31
lines changed

2 files changed

+69
-31
lines changed

src/main/java/io/zentity/resolution/Job.java

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import org.elasticsearch.search.builder.SearchSourceBuilder;
3434

3535
import java.io.IOException;
36+
import java.io.PrintWriter;
37+
import java.io.StringWriter;
3638
import java.util.ArrayList;
3739
import java.util.Arrays;
3840
import java.util.Base64;
@@ -53,6 +55,7 @@ public class Job {
5355

5456
// Constants
5557
public static final boolean DEFAULT_INCLUDE_ATTRIBUTES = true;
58+
public static final boolean DEFAULT_INCLUDE_ERROR_TRACE = true;
5659
public static final boolean DEFAULT_INCLUDE_EXPLANATION = false;
5760
public static final boolean DEFAULT_INCLUDE_HITS = true;
5861
public static final boolean DEFAULT_INCLUDE_QUERIES = false;
@@ -65,6 +68,7 @@ public class Job {
6568
// Job configuration
6669
private Input input;
6770
private boolean includeAttributes = DEFAULT_INCLUDE_ATTRIBUTES;
71+
private boolean includeErrorTrace = DEFAULT_INCLUDE_ERROR_TRACE;
6872
private boolean includeExplanation = DEFAULT_INCLUDE_EXPLANATION;
6973
private boolean includeHits = DEFAULT_INCLUDE_HITS;
7074
private boolean includeQueries = DEFAULT_INCLUDE_QUERIES;
@@ -79,6 +83,7 @@ public class Job {
7983
private NodeClient client;
8084
private Map<String, Set<String>> docIds = new TreeMap<>();
8185
private String error = null;
86+
private boolean failed = false;
8287
private List<String> hits = new ArrayList<>();
8388
private int hop = 0;
8489
private Set<String> missingIndices = new TreeSet<>();
@@ -89,16 +94,20 @@ public Job(NodeClient client) {
8994
this.client = client;
9095
}
9196

92-
public static String serializeException(Exception e) throws IOException {
93-
String serialized;
94-
if (e instanceof ElasticsearchException) {
95-
ElasticsearchException ee = (ElasticsearchException) e;
96-
String cause = Strings.toString(ee.toXContent(jsonBuilder().startObject(), ToXContent.EMPTY_PARAMS).endObject());
97-
serialized = "{\"error\":{\"root_cause\":[" + cause + "],\"type\":\"" + ElasticsearchException.getExceptionName(ee) + "\",\"reason\":\"" + e.getMessage() + "\"},\"status\":" + ee.status().getStatus() + "}";
98-
} else {
99-
serialized = "{\"error\":{\"type\":\"" + e.getClass() + "\",\"reason\":\"" + e.getMessage() + "\"}}";
97+
public static String serializeException(Exception e, boolean includeErrorTrace) {
98+
List<String> errorParts = new ArrayList<>();
99+
if (e instanceof ElasticsearchException)
100+
errorParts.add("\"by\":\"elasticsearch\"");
101+
else
102+
errorParts.add("\"by\":\"zentity\"");
103+
errorParts.add("\"type\":\"" + e.getClass().getCanonicalName() + "\"");
104+
errorParts.add("\"reason\":\"" + e.getMessage() + "\"");
105+
if (includeErrorTrace) {
106+
StringWriter traceWriter = new StringWriter();
107+
e.printStackTrace(new PrintWriter(traceWriter));
108+
errorParts.add("\"stack_trace\":" + Json.quoteString(traceWriter.toString()) + "");
100109
}
101-
return serialized;
110+
return String.join(",", errorParts);
102111
}
103112

104113
public static String serializeLoggedQuery(Input input, int _hop, int _query, String indexName, String request, String response, List<String> resolvers, TreeMap<Integer, TreeMap<String, TreeMap>> resolversFilterTreeGrouped, List<String> termResolvers, TreeMap<String, TreeMap> termResolversFilterTree) throws JsonProcessingException {
@@ -522,6 +531,7 @@ private void resetState() {
522531
this.attributes = new TreeMap<>(this.input().attributes());
523532
this.docIds = new TreeMap<>();
524533
this.error = null;
534+
this.failed = false;
525535
this.hits = new ArrayList<>();
526536
this.hop = 0;
527537
this.missingIndices = new TreeSet<>();
@@ -537,6 +547,14 @@ public void includeAttributes(boolean includeAttributes) {
537547
this.includeAttributes = includeAttributes;
538548
}
539549

550+
public boolean includeErrorTrace() {
551+
return this.includeErrorTrace;
552+
}
553+
554+
public void includeErrorTrace(boolean includeErrorTrace) {
555+
this.includeErrorTrace = includeErrorTrace;
556+
}
557+
540558
public boolean includeExplanation() {
541559
return this.includeExplanation;
542560
}
@@ -609,6 +627,14 @@ public void input(Input input) {
609627
this.input = input;
610628
}
611629

630+
public boolean failed() {
631+
return this.failed;
632+
}
633+
634+
public boolean ran() {
635+
return this.ran;
636+
}
637+
612638
/**
613639
* Submit a search query to Elasticsearch.
614640
*
@@ -997,7 +1023,6 @@ else if (!resolversClause.isEmpty())
9971023
// Submit query to Elasticsearch.
9981024
SearchResponse response = null;
9991025
Exception responseError = null;
1000-
boolean fatalError = false;
10011026
try {
10021027
response = this.search(indexName, query);
10031028
} catch (IndexNotFoundException e) {
@@ -1006,7 +1031,7 @@ else if (!resolversClause.isEmpty())
10061031
responseError = e;
10071032
} catch (Exception e) {
10081033
// Fail the job for any other error.
1009-
fatalError = true;
1034+
this.failed = true;
10101035
responseError = e;
10111036
}
10121037

@@ -1028,16 +1053,18 @@ else if (!resolversClause.isEmpty())
10281053
}
10291054
responseString = responseDataCopyObj.toString();
10301055
} else {
1031-
responseString = serializeException(responseError);
1056+
ElasticsearchException e = (ElasticsearchException) responseError;
1057+
String cause = Strings.toString(e.toXContent(jsonBuilder().startObject(), ToXContent.EMPTY_PARAMS).endObject());
1058+
responseString = "{\"error\":{\"root_cause\":[" + cause + "],\"type\":\"" + ElasticsearchException.getExceptionName(e) + "\",\"reason\":\"" + e.getMessage() + "\"},\"status\":" + e.status().getStatus() + "}";
10321059
}
10331060
String logged = serializeLoggedQuery(this.input, this.hop, _query, indexName, query, responseString, resolvers, resolversFilterTreeGrouped, termResolvers, termResolversFilterTree);
10341061
this.queries.add(logged);
10351062
}
10361063

10371064
// Stop traversing if there was an error not due to a missing index.
10381065
// Include the logged query in the response.
1039-
if (fatalError) {
1040-
this.error = serializeLoggedQuery(this.input, this.hop, _query, indexName, query, serializeException(responseError), resolvers, resolversFilterTreeGrouped, termResolvers, termResolversFilterTree);
1066+
if (this.failed) {
1067+
this.error = serializeException(responseError, this.includeErrorTrace);
10411068
return;
10421069
}
10431070

@@ -1228,7 +1255,7 @@ else if (input.model().matchers().containsKey(matcherName))
12281255
* @return A JSON string to be returned as the body of the response to a client.
12291256
* @throws IOException
12301257
*/
1231-
public String run() throws IOException, ValidationException {
1258+
public String run() throws IOException {
12321259
try {
12331260

12341261
// Reset the state of the job if reusing this Job object.
@@ -1238,22 +1265,28 @@ public String run() throws IOException, ValidationException {
12381265
this.attributes = new TreeMap<>(this.input.attributes());
12391266

12401267
// Start timer and begin job
1268+
String response;
12411269
long startTime = System.nanoTime();
1242-
this.traverse();
1243-
long took = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
1244-
1245-
// Format response
1246-
List<String> responseParts = new ArrayList<>();
1247-
responseParts.add("\"took\":" + Long.toString(took));
1248-
if (this.error != null)
1249-
responseParts.add("\"error\":" + this.error);
1250-
if (this.includeHits)
1251-
responseParts.add("\"hits\":{\"total\":" + this.hits.size() + ",\"hits\":[" + String.join(",", this.hits) + "]}");
1252-
if (this.includeQueries || this.profile)
1253-
responseParts.add("\"queries\":[" + queries + "]");
1254-
String response = "{" + String.join(",", responseParts) + "}";
1255-
if (this.pretty)
1256-
response = Json.ORDERED_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(Json.ORDERED_MAPPER.readTree(response));
1270+
try {
1271+
this.traverse();
1272+
} catch (Exception e) {
1273+
this.failed = true;
1274+
this.error = serializeException(e, this.includeErrorTrace);
1275+
} finally {
1276+
long took = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
1277+
// Format response
1278+
List<String> responseParts = new ArrayList<>();
1279+
responseParts.add("\"took\":" + Long.toString(took));
1280+
if (this.error != null)
1281+
responseParts.add("\"error\":{" + this.error + "}");
1282+
if (this.includeHits)
1283+
responseParts.add("\"hits\":{\"total\":" + this.hits.size() + ",\"hits\":[" + String.join(",", this.hits) + "]}");
1284+
if (this.includeQueries || this.profile)
1285+
responseParts.add("\"queries\":[" + queries + "]");
1286+
response = "{" + String.join(",", responseParts) + "}";
1287+
if (this.pretty)
1288+
response = Json.ORDERED_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(Json.ORDERED_MAPPER.readTree(response));
1289+
}
12571290
return response;
12581291

12591292
} finally {

src/main/java/org/elasticsearch/plugin/zentity/ResolutionAction.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient
3838
// Parse the request params that will be passed to the job configuration
3939
String entityType = restRequest.param("entity_type");
4040
Boolean includeAttributes = restRequest.paramAsBoolean("_attributes", Job.DEFAULT_INCLUDE_ATTRIBUTES);
41+
Boolean includeErrorTrace = restRequest.paramAsBoolean("error_trace", Job.DEFAULT_INCLUDE_ERROR_TRACE);
4142
Boolean includeExplanation = restRequest.paramAsBoolean("_explanation", Job.DEFAULT_INCLUDE_EXPLANATION);
4243
Boolean includeHits = restRequest.paramAsBoolean("hits", Job.DEFAULT_INCLUDE_HITS);
4344
Boolean includeQueries = restRequest.paramAsBoolean("queries", Job.DEFAULT_INCLUDE_QUERIES);
@@ -70,6 +71,7 @@ protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient
7071
// Prepare the entity resolution job.
7172
Job job = new Job(client);
7273
job.includeAttributes(includeAttributes);
74+
job.includeErrorTrace(includeErrorTrace);
7375
job.includeExplanation(includeExplanation);
7476
job.includeHits(includeHits);
7577
job.includeQueries(includeQueries);
@@ -82,7 +84,10 @@ protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient
8284

8385
// Run the entity resolution job.
8486
String response = job.run();
85-
channel.sendResponse(new BytesRestResponse(RestStatus.OK, "application/json", response));
87+
if (job.failed())
88+
channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "application/json", response));
89+
else
90+
channel.sendResponse(new BytesRestResponse(RestStatus.OK, "application/json", response));
8691

8792
} catch (ValidationException e) {
8893
channel.sendResponse(new BytesRestResponse(channel, RestStatus.BAD_REQUEST, e));

0 commit comments

Comments
 (0)