fix(query): preserve structured backend errors
This commit is contained in:
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",
|
||||
|
||||
Reference in new issue
Block a user