fix(athena): report a failed query's error the way Athena does
StateChangeReason carried the raw engine call failure, HTTP envelope and all:
floci-duck execute returned HTTP 500: {"status":"error","message":...},
ending with the statement as the engine saw it, wrapped in COPY (...) TO. It is
now the engine's own message alone, such as Catalog Error: Table with name nope
does not exist!
This commit is contained in:
@@ -59,6 +59,8 @@ public class AthenaService {
|
|||||||
public static final String DEFAULT_CATALOG = "AwsDataCatalog";
|
public static final String DEFAULT_CATALOG = "AwsDataCatalog";
|
||||||
private static final String DEFAULT_OUTPUT_BUCKET = "floci-athena-results";
|
private static final String DEFAULT_OUTPUT_BUCKET = "floci-athena-results";
|
||||||
private static final String DEFAULT_WORKGROUP = "primary";
|
private static final String DEFAULT_WORKGROUP = "primary";
|
||||||
|
private static final Pattern ENGINE_FAILURE = Pattern.compile("^floci-duck \\w+ returned HTTP \\d+: (\\{.*})$", Pattern.DOTALL);
|
||||||
|
private static final com.fasterxml.jackson.databind.ObjectMapper FAILURE_READER = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
private static final Pattern TRAILING_SEMICOLONS = Pattern.compile("[\\s;]+$");
|
private static final Pattern TRAILING_SEMICOLONS = Pattern.compile("[\\s;]+$");
|
||||||
private static final String DEFAULT_ENGINE_VERSION = "Athena engine version 3";
|
private static final String DEFAULT_ENGINE_VERSION = "Athena engine version 3";
|
||||||
private static final long MIN_BYTES_SCANNED_CUTOFF_PER_QUERY = 10_000_000L;
|
private static final long MIN_BYTES_SCANNED_CUTOFF_PER_QUERY = 10_000_000L;
|
||||||
@@ -610,10 +612,35 @@ public class AthenaService {
|
|||||||
|
|
||||||
private void markFailed(String id, QueryExecution execution, Throwable failure) {
|
private void markFailed(String id, QueryExecution execution, Throwable failure) {
|
||||||
execution.getStatus().setState(QueryExecutionState.FAILED);
|
execution.getStatus().setState(QueryExecutionState.FAILED);
|
||||||
execution.getStatus().setStateChangeReason(failure.getMessage());
|
execution.getStatus().setStateChangeReason(failureReason(failure.getMessage()));
|
||||||
queryStore.put(id, execution);
|
queryStore.put(id, execution);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The engine's own explanation of a failed query, as Athena reports it: without the HTTP
|
||||||
|
* envelope of the engine call, and without the echoed statement, which shows the
|
||||||
|
* internal {@code COPY ... TO} wrapper rather than the query that was sent.
|
||||||
|
*/
|
||||||
|
static String failureReason(String message) {
|
||||||
|
if (message == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Matcher envelope = ENGINE_FAILURE.matcher(message);
|
||||||
|
if (!envelope.matches()) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String engineMessage = FAILURE_READER.readTree(envelope.group(1)).path("message").asText("");
|
||||||
|
if (engineMessage.isBlank()) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
int echo = engineMessage.indexOf("\n\nLINE ");
|
||||||
|
return (echo >= 0 ? engineMessage.substring(0, echo) : engineMessage).strip();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private record CreateDatabaseDdl(String name, boolean ifNotExists, String comment,
|
private record CreateDatabaseDdl(String name, boolean ifNotExists, String comment,
|
||||||
String location, Map<String, String> properties) {
|
String location, Map<String, String> properties) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package io.github.hectorvent.floci.services.athena;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
class AthenaQueryTextTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dropsTheClosingSemicolonsAthenaAccepts() {
|
||||||
|
assertEquals("SELECT * FROM orders LIMIT 10", AthenaService.withoutTrailingSemicolons("SELECT * FROM orders LIMIT 10;"));
|
||||||
|
assertEquals("SELECT 1", AthenaService.withoutTrailingSemicolons("SELECT 1 ; ;\n"));
|
||||||
|
assertEquals("SELECT ';' AS s", AthenaService.withoutTrailingSemicolons("SELECT ';' AS s"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reportsTheEngineErrorWithoutItsEnvelopeOrTheWrappedStatement() {
|
||||||
|
String engine = "floci-duck execute returned HTTP 500: {\"status\":\"error\",\"message\":\"Catalog Error: Table with name nope does not exist!\\nDid you mean \\\"orders\\\"?\\n\\nLINE 1: COPY (SELECT * FROM nope) TO 's3://results/q.csv'\"}";
|
||||||
|
assertEquals("Catalog Error: Table with name nope does not exist!\nDid you mean \"orders\"?", AthenaService.failureReason(engine));
|
||||||
|
assertEquals("Failed to call floci-duck execute: timeout", AthenaService.failureReason("Failed to call floci-duck execute: timeout"));
|
||||||
|
}
|
||||||
|
}
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
package io.github.hectorvent.floci.services.athena;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
class AthenaTrailingSemicolonTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void dropsTheClosingSemicolonsAthenaAccepts() {
|
|
||||||
assertEquals("SELECT * FROM orders LIMIT 10", AthenaService.withoutTrailingSemicolons("SELECT * FROM orders LIMIT 10;"));
|
|
||||||
assertEquals("SELECT 1", AthenaService.withoutTrailingSemicolons("SELECT 1 ; ;\n"));
|
|
||||||
assertEquals("SELECT ';' AS s", AthenaService.withoutTrailingSemicolons("SELECT ';' AS s"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user