fix(query): preserve structured backend errors

This commit is contained in:
miracle authored and GitHub committed 2026-08-02 16:37:46 +08:00
1 parent 2e9dcdc26b
commit bcafc4a474
58 files changed
+4182 -554

No files matched your search

@@ -113,6 +113,7 @@ public final class AgentProtocol {
public static final String CAPABILITY_ETCD_LEASE = "etcd_lease";
public static final String CAPABILITY_ETCD_AUTH = "etcd_auth";
public static final String CAPABILITY_MULTI_SESSION = "multi_session";
public static final String CAPABILITY_STRUCTURED_ERROR_V1 = "structured_error_v1";
public static final List<String> CAPABILITIES = Collections.unmodifiableList(Arrays.asList(
CAPABILITY_CONNECT,
@@ -147,6 +148,8 @@ public final class AgentProtocol {
public static final List<String> MULTI_SESSION_CAPABILITIES;
public static final List<String> MULTI_SESSION_ALL_CAPABILITIES;
public static final List<String> MULTI_SESSION_JDBC_CAPABILITIES;
public static final List<String> MULTI_SESSION_JDBC_ALL_CAPABILITIES;
public static final List<String> COMMON_METHODS = Collections.unmodifiableList(Arrays.asList(
METHOD_HANDSHAKE,
@@ -194,6 +197,14 @@ public final class AgentProtocol {
allCapabilities.add(CAPABILITY_MULTI_SESSION);
MULTI_SESSION_ALL_CAPABILITIES = Collections.unmodifiableList(allCapabilities);
List<String> jdbcCapabilities = new java.util.ArrayList<>(MULTI_SESSION_CAPABILITIES);
jdbcCapabilities.add(CAPABILITY_STRUCTURED_ERROR_V1);
MULTI_SESSION_JDBC_CAPABILITIES = Collections.unmodifiableList(jdbcCapabilities);
List<String> jdbcAllCapabilities = new java.util.ArrayList<>(MULTI_SESSION_ALL_CAPABILITIES);
jdbcAllCapabilities.add(CAPABILITY_STRUCTURED_ERROR_V1);
MULTI_SESSION_JDBC_ALL_CAPABILITIES = Collections.unmodifiableList(jdbcAllCapabilities);
List<String> methods = new java.util.ArrayList<>(COMMON_METHODS);
int insertAt = methods.indexOf(METHOD_CONNECT) + 1;
methods.addAll(insertAt, Arrays.asList(
@@ -270,6 +281,18 @@ public final class AgentProtocol {
);
}
/**
* Handshake for pooled JDBC Agents that emit the v1 structured error contract.
* Generic/custom v2 handlers must continue using multiSessionHandshakeResult().
*/
public static HandshakeResult multiSessionJdbcHandshakeResult() {
return new HandshakeResult(
MULTI_SESSION_PROTOCOL_VERSION,
MULTI_SESSION_PROTOCOL_VERSION,
MULTI_SESSION_JDBC_CAPABILITIES
);
}
public static final class HandshakeResult {
private final int protocolVersion;
private final int agentProtocolVersion;
@@ -3,15 +3,21 @@ package com.dbx.agent;
import com.google.gson.JsonObject;
import java.sql.SQLException;
import java.sql.SQLTimeoutException;
import java.sql.SQLRecoverableException;
import java.sql.SQLTransientConnectionException;
import java.util.Locale;
import java.util.concurrent.CancellationException;
final class AgentRpcError extends RuntimeException {
private final String category;
private final boolean retryable;
private final String disposition;
private final String stage;
private final String operationOutcome;
private final String sqlState;
private final Integer vendorCode;
private final String exceptionClass;
private AgentRpcError(
String message,
@@ -19,6 +25,10 @@ final class AgentRpcError extends RuntimeException {
boolean retryable,
String disposition,
String stage,
String operationOutcome,
String sqlState,
Integer vendorCode,
String exceptionClass,
Throwable cause
) {
super(message, cause);
@@ -26,6 +36,10 @@ final class AgentRpcError extends RuntimeException {
this.retryable = retryable;
this.disposition = disposition;
this.stage = stage;
this.operationOutcome = operationOutcome;
this.sqlState = sqlState;
this.vendorCode = vendorCode;
this.exceptionClass = exceptionClass;
}
static AgentRpcError resource(String stage, Throwable cause) {
@@ -35,6 +49,10 @@ final class AgentRpcError extends RuntimeException {
false,
"replace_runtime",
stage,
operationOutcome(stage),
null,
null,
cause == null ? null : cause.getClass().getName(),
cause
);
}
@@ -46,6 +64,10 @@ final class AgentRpcError extends RuntimeException {
true,
"keep",
stage,
operationOutcome(stage),
null,
null,
cause == null ? null : cause.getClass().getName(),
cause
);
}
@@ -60,6 +82,13 @@ final class AgentRpcError extends RuntimeException {
data.addProperty("retryable", classified.retryable);
data.addProperty("sessionDisposition", classified.disposition);
data.addProperty("stage", classified.stage);
data.addProperty("contractVersion", 1);
data.addProperty("operationOutcome", classified.operationOutcome);
addDiagnostic(data, "sqlState", classified.sqlState);
if (classified.vendorCode != null) {
data.addProperty("vendorCode", classified.vendorCode);
}
addDiagnostic(data, "exceptionClass", classified.exceptionClass);
if (agentSessionId != null && !agentSessionId.trim().isEmpty()) {
data.addProperty("agentSessionId", agentSessionId);
}
@@ -72,34 +101,69 @@ final class AgentRpcError extends RuntimeException {
if (explicit != null) {
return explicit;
}
String stage = stage(method);
if (find(error, CancellationException.class) != null || find(error, InterruptedException.class) != null) {
return new AgentRpcError(
message(error),
"canceled",
false,
"quarantine",
stage,
operationOutcome(stage),
null,
null,
safeClassName(error),
error
);
}
SQLException sqlError = find(error, SQLException.class);
if (sqlError != null) {
String sqlState = sqlError.getSQLState();
String stage = stage(method);
String category = sqlError instanceof SQLTimeoutException ? "timeout" : null;
boolean connectionError = "connect".equals(stage)
|| "validate".equals(stage)
|| sqlError instanceof SQLRecoverableException
|| sqlError instanceof SQLTransientConnectionException
|| (sqlState != null && sqlState.toUpperCase(Locale.ROOT).startsWith("08"));
boolean operationRetryable = connectionError && ("connect".equals(stage) || "validate".equals(stage));
String disposition = connectionError && !"connect".equals(stage) ? "quarantine" : "keep";
String disposition = category != null || (connectionError && !"connect".equals(stage)) ? "quarantine" : "keep";
return new AgentRpcError(
message(error),
connectionError ? "connection" : "sql",
category == null ? (connectionError ? "connection" : "sql") : category,
operationRetryable,
disposition,
stage,
operationOutcome(stage),
safeSqlState(sqlState),
sqlError.getErrorCode(),
safeClassName(sqlError),
error
);
}
return new AgentRpcError(message(error), "protocol", false, "keep", stage(method), error);
return new AgentRpcError(
message(error),
"protocol",
false,
"keep",
stage,
operationOutcome(stage),
null,
null,
safeClassName(error),
error
);
}
private static String stage(String method) {
if (method == null) {
return "request";
}
if (AgentProtocol.METHOD_CONNECT.equals(method) || AgentProtocol.METHOD_OPEN_SESSION.equals(method)) {
if (AgentProtocol.METHOD_HANDSHAKE.equals(method)) {
return "request";
}
if (AgentProtocol.METHOD_CONNECT.equals(method)
|| AgentProtocol.METHOD_OPEN_SESSION.equals(method)
|| AgentProtocol.METHOD_TEST_CONNECTION.equals(method)) {
return "connect";
}
if (AgentProtocol.METHOD_VALIDATE_CONNECTION.equals(method) || AgentProtocol.METHOD_VALIDATE_SESSION.equals(method)) {
@@ -108,7 +172,11 @@ final class AgentRpcError extends RuntimeException {
if (AgentProtocol.METHOD_CANCEL_SESSION.equals(method)) {
return "cancel";
}
if (AgentProtocol.METHOD_CLOSE_SESSION.equals(method) || AgentProtocol.METHOD_DISCONNECT.equals(method)) {
if (AgentProtocol.METHOD_CLOSE_SESSION.equals(method)
|| AgentProtocol.METHOD_DISCONNECT.equals(method)
|| AgentProtocol.METHOD_CLOSE_QUERY_SESSION.equals(method)
|| AgentProtocol.METHOD_CLOSE_TABLE_READ_SESSION.equals(method)
|| AgentProtocol.METHOD_SHUTDOWN.equals(method)) {
return "close";
}
if (AgentProtocol.METHOD_FETCH_QUERY_PAGE.equals(method)
@@ -122,6 +190,41 @@ final class AgentRpcError extends RuntimeException {
return error.getMessage() == null ? error.toString() : error.getMessage();
}
private static String operationOutcome(String stage) {
return switch (stage) {
case "request", "checkout", "connect", "validate" -> "not_started";
default -> "unknown";
};
}
private static String safeSqlState(String sqlState) {
return safeDiagnostic(sqlState, 16);
}
private static String safeClassName(Throwable error) {
return error == null ? null : safeDiagnostic(error.getClass().getName(), 160);
}
private static String safeDiagnostic(String value, int maxLength) {
if (value == null) {
return null;
}
StringBuilder safe = new StringBuilder(Math.min(value.length(), maxLength));
for (int index = 0; index < value.length() && safe.length() < maxLength; index++) {
char character = value.charAt(index);
if (character >= 0x21 && character <= 0x7e) {
safe.append(character);
}
}
return safe.isEmpty() ? null : safe.toString();
}
private static void addDiagnostic(JsonObject data, String name, String value) {
if (value != null && !value.isBlank()) {
data.addProperty(name, value);
}
}
private static <T extends Throwable> T find(Throwable error, Class<T> type) {
Throwable current = error;
while (current != null) {
@@ -27,7 +27,7 @@ import java.util.function.Supplier;
public final class MultiSessionJsonRpcServer implements AutoCloseable {
private static final String LEGACY_SESSION_ID = "__legacy__";
private static final int MAX_SESSIONS = 256;
static final int MAX_SESSIONS = 256;
private static final int MAX_REQUEST_THREADS = 64;
private static final int MAX_CLEANUP_THREADS = 16;
private static final long MAINTENANCE_INTERVAL_MILLIS = 60_000L;
@@ -137,7 +137,7 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
try {
Object result;
if (AgentProtocol.METHOD_HANDSHAKE.equals(method)) {
result = sessionHandlerFactory == null ? AgentProtocol.multiSessionHandshakeResult() : customHandshake();
result = sessionHandlerFactory == null ? AgentProtocol.multiSessionJdbcHandshakeResult() : customHandshake();
} else if (AgentProtocol.METHOD_OPEN_SESSION.equals(method)) {
result = openSession(requiredSessionId(params), params);
} else if (AgentProtocol.METHOD_CLOSE_SESSION.equals(method)) {
@@ -171,7 +171,10 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
private Object openSession(String sessionId, JsonObject params) throws Exception {
if (sessions.size() >= MAX_SESSIONS && !sessions.containsKey(sessionId)) {
throw new IllegalStateException("Agent session limit reached: " + MAX_SESSIONS);
throw AgentRpcError.backpressure(
"connect",
new IllegalStateException("Agent session limit reached: " + MAX_SESSIONS)
);
}
Session session;
if (sessionHandlerFactory != null) {
@@ -2,9 +2,9 @@
"protocolVersion": 2,
"handshakeMethod": "handshake",
"handshakeResponseFields": ["protocolVersion", "agentProtocolVersion", "capabilities"],
"allCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "kv", "kv_ttl", "kv_cas", "kv_list_values", "kv_status", "kv_history", "etcd_compaction", "etcd_defrag", "etcd_watch", "etcd_lease", "etcd_auth", "multi_session"],
"allCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "kv", "kv_ttl", "kv_cas", "kv_list_values", "kv_status", "kv_history", "etcd_compaction", "etcd_defrag", "etcd_watch", "etcd_lease", "etcd_auth", "multi_session", "structured_error_v1"],
"capabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "multi_session"],
"defaultSqlCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "multi_session"],
"defaultSqlCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "multi_session", "structured_error_v1"],
"commonMethods": [
"handshake",
"connect",