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:
Zakaria
2026-10-25 18:07:31 -04:00
parent ac96c6451e
commit 57aa866144
3 changed files with 50 additions and 16 deletions
@@ -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"));
}
}
@@ -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"));
}
}