fix(agent): recover JDBC connections after query timeouts

This commit is contained in:
miracle authored and GitHub committed 2026-08-01 01:17:49 +08:00
1 parent 0d484f5c74
commit 0e414e638e
24 files changed
+5983 -473

No files matched your search

@@ -29,6 +29,7 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
private boolean requestActive;
private boolean leasePinnedAtRequestStart;
private boolean sessionAffinity;
private boolean pooledConnectionPoisoned;
@Override
public final Connection getConnection() {
@@ -47,6 +48,7 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
sessionAffinity = false;
requestActive = false;
leasePinnedAtRequestStart = false;
pooledConnectionPoisoned = false;
loadDriver(params);
configuredDatabase = params.getDatabase();
connectParams = params;
@@ -256,6 +258,7 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
sessionAffinity = false;
requestActive = false;
leasePinnedAtRequestStart = false;
pooledConnectionPoisoned = false;
identifierQuote = "";
});
}
@@ -271,24 +274,54 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
return poolRegistry != null;
}
final synchronized void beginPooledRequest() throws Exception {
if (poolRegistry == null) {
return;
}
if (connectParams == null) {
throw new IllegalStateException("Not connected");
}
requestActive = true;
leasePinnedAtRequestStart = pooledLease != null;
if (pooledLease == null) {
try {
pooledLease = borrowPooledConnection();
} catch (Exception error) {
requestActive = false;
throw error;
final synchronized boolean quarantinePooledConnection() {
pooledConnectionPoisoned = true;
return requestActive && pooledLease != null && pooledLease.quarantine();
}
final void beginPooledRequest() throws Exception {
ConnectParams params;
String identity;
JdbcConnectionPoolRegistry registry;
synchronized (this) {
if (poolRegistry == null) {
return;
}
if (connectParams == null || poolIdentity == null) {
throw new IllegalStateException("Not connected");
}
requestActive = true;
leasePinnedAtRequestStart = pooledLease != null;
if (pooledLease != null) {
connection = pooledLease.connection();
return;
}
params = connectParams;
identity = poolIdentity;
registry = poolRegistry;
}
JdbcConnectionPoolRegistry.Lease borrowed;
try {
borrowed = borrowPooledConnection(registry, identity, params);
} catch (Exception error) {
synchronized (this) {
requestActive = false;
leasePinnedAtRequestStart = false;
}
throw error;
}
synchronized (this) {
if (pooledConnectionPoisoned) {
requestActive = false;
leasePinnedAtRequestStart = false;
borrowed.evict();
throw new IllegalStateException("JDBC Session was quarantined while waiting for a connection");
}
pooledLease = borrowed;
connection = borrowed.connection();
}
connection = pooledLease.connection();
}
final synchronized void finishPooledRequest(
@@ -301,6 +334,10 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
return;
}
requestActive = false;
if (pooledConnectionPoisoned) {
releasePooledConnection(true);
return;
}
if (succeeded && requiresSessionAffinity) {
sessionAffinity = true;
JdbcSchemaSwitcher.forget(connection);
@@ -322,7 +359,14 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
}
final synchronized void releaseIdlePooledConnection(JdbcExecutor executor) {
if (poolRegistry == null || requestActive || sessionAffinity || pooledLease == null) {
if (poolRegistry == null || requestActive || pooledLease == null) {
return;
}
if (pooledConnectionPoisoned) {
releasePooledConnection(true);
return;
}
if (sessionAffinity) {
return;
}
if (executor.hasOpenSessions() || executor.hasActiveStatements()) {
@@ -489,18 +533,20 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
private Connection openInitializedConnection(ConnectParams params) throws Exception {
Connection opened = openConnection(params);
boolean initialized = false;
try {
afterPhysicalConnect(params, opened);
initialized = true;
return opened;
} finally {
if (!initialized) {
try {
opened.close();
} catch (Exception ignored) {
}
} catch (Exception error) {
try {
opened.close();
} catch (Exception closeError) {
error.addSuppressed(closeError);
throw AgentRpcError.resource(
"close",
new JdbcConnectionPoolRegistry.PhysicalConnectionStateUnknownException(error)
);
}
throw error;
}
}
@@ -509,12 +555,24 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
if (params == null || poolIdentity == null || poolRegistry == null) {
throw new IllegalStateException("Not connected");
}
return poolRegistry.borrow(poolIdentity, () -> openInitializedConnection(params));
return borrowPooledConnection(poolRegistry, poolIdentity, params);
}
private JdbcConnectionPoolRegistry.Lease borrowPooledConnection(
JdbcConnectionPoolRegistry registry,
String identity,
ConnectParams params
) throws Exception {
return registry.borrow(
identity,
JdbcSessionRole.from(params.getSessionRole()),
() -> openInitializedConnection(params)
);
}
private void closeCurrentConnection() throws Exception {
if (pooledLease != null) {
boolean evict = sessionAffinity || !preparePooledConnectionForReturn();
boolean evict = pooledConnectionPoisoned || sessionAffinity || !preparePooledConnectionForReturn();
releasePooledConnection(evict);
} else if (connection != null) {
connection.close();
@@ -0,0 +1,135 @@
package com.dbx.agent;
import com.google.gson.JsonObject;
import java.sql.SQLException;
import java.sql.SQLRecoverableException;
import java.sql.SQLTransientConnectionException;
import java.util.Locale;
final class AgentRpcError extends RuntimeException {
private final String category;
private final boolean retryable;
private final String disposition;
private final String stage;
private AgentRpcError(
String message,
String category,
boolean retryable,
String disposition,
String stage,
Throwable cause
) {
super(message, cause);
this.category = category;
this.retryable = retryable;
this.disposition = disposition;
this.stage = stage;
}
static AgentRpcError resource(String stage, Throwable cause) {
return new AgentRpcError(
"Agent runtime resource limit reached",
"resource",
false,
"replace_runtime",
stage,
cause
);
}
static AgentRpcError backpressure(String stage, Throwable cause) {
return new AgentRpcError(
"Agent request capacity is temporarily exhausted",
"resource",
true,
"keep",
stage,
cause
);
}
static JsonObject toJson(Throwable error, String method, String agentSessionId) {
AgentRpcError classified = classify(error, method);
JsonObject rpcError = new JsonObject();
rpcError.addProperty("code", -1);
rpcError.addProperty("message", message(error));
JsonObject data = new JsonObject();
data.addProperty("category", classified.category);
data.addProperty("retryable", classified.retryable);
data.addProperty("sessionDisposition", classified.disposition);
data.addProperty("stage", classified.stage);
if (agentSessionId != null && !agentSessionId.trim().isEmpty()) {
data.addProperty("agentSessionId", agentSessionId);
}
rpcError.add("data", data);
return rpcError;
}
private static AgentRpcError classify(Throwable error, String method) {
AgentRpcError explicit = find(error, AgentRpcError.class);
if (explicit != null) {
return explicit;
}
SQLException sqlError = find(error, SQLException.class);
if (sqlError != null) {
String sqlState = sqlError.getSQLState();
String stage = stage(method);
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";
return new AgentRpcError(
message(error),
connectionError ? "connection" : "sql",
operationRetryable,
disposition,
stage,
error
);
}
return new AgentRpcError(message(error), "protocol", false, "keep", stage(method), error);
}
private static String stage(String method) {
if (method == null) {
return "request";
}
if (AgentProtocol.METHOD_CONNECT.equals(method) || AgentProtocol.METHOD_OPEN_SESSION.equals(method)) {
return "connect";
}
if (AgentProtocol.METHOD_VALIDATE_CONNECTION.equals(method) || AgentProtocol.METHOD_VALIDATE_SESSION.equals(method)) {
return "validate";
}
if (AgentProtocol.METHOD_CANCEL_SESSION.equals(method)) {
return "cancel";
}
if (AgentProtocol.METHOD_CLOSE_SESSION.equals(method) || AgentProtocol.METHOD_DISCONNECT.equals(method)) {
return "close";
}
if (AgentProtocol.METHOD_FETCH_QUERY_PAGE.equals(method)
|| AgentProtocol.METHOD_FETCH_TABLE_READ_PAGE.equals(method)) {
return "fetch";
}
return "execute";
}
private static String message(Throwable error) {
return error.getMessage() == null ? error.toString() : error.getMessage();
}
private static <T extends Throwable> T find(Throwable error, Class<T> type) {
Throwable current = error;
while (current != null) {
if (type.isInstance(current)) {
return type.cast(current);
}
current = current.getCause();
}
return null;
}
}
@@ -22,6 +22,7 @@ public final class ConnectParams {
private String client_key_path;
private String gbase_server;
private String informix_server;
private String sessionRole;
public ConnectParams() {
this("", 0, "", "", "", "", "", false, "", Collections.emptyList());
@@ -200,6 +201,14 @@ public final class ConnectParams {
this.informix_server = informix_server;
}
public String getSessionRole() {
return sessionRole;
}
public void setSessionRole(String sessionRole) {
this.sessionRole = sessionRole;
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
@@ -221,13 +230,14 @@ public final class ConnectParams {
&& Objects.equals(client_cert_path, that.client_cert_path)
&& Objects.equals(client_key_path, that.client_key_path)
&& Objects.equals(gbase_server, that.gbase_server)
&& Objects.equals(informix_server, that.informix_server);
&& Objects.equals(informix_server, that.informix_server)
&& Objects.equals(sessionRole, that.sessionRole);
}
@Override
public int hashCode() {
return Objects.hash(host, port, database, username, password, url_params, connection_string,
port_explicit, mysql_compat_mode, jdbc_driver_class, jdbc_driver_paths, ssl, ca_cert_path, client_cert_path, client_key_path, gbase_server, informix_server);
port_explicit, mysql_compat_mode, jdbc_driver_class, jdbc_driver_paths, ssl, ca_cert_path, client_cert_path, client_key_path, gbase_server, informix_server, sessionRole);
}
@Override
@@ -249,6 +259,7 @@ public final class ConnectParams {
+ ", client_key_path=" + client_key_path
+ ", gbase_server=" + gbase_server
+ ", informix_server=" + informix_server
+ ", sessionRole=" + sessionRole
+ ")";
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,10 @@
package com.dbx.agent;
enum JdbcSessionRole {
WORKLOAD,
METADATA;
static JdbcSessionRole from(String value) {
return "metadata".equalsIgnoreCase(value == null ? "" : value.trim()) ? METADATA : WORKLOAD;
}
}
@@ -121,6 +121,11 @@ public final class JsonRpcServer {
}
}
boolean quarantine() {
AbstractJdbcAgent jdbcAgent = pooledJdbcAgent();
return jdbcAgent != null && jdbcAgent.quarantinePooledConnection();
}
void expireIdleResources() {
jdbcExecutor.expireIdleResources();
releaseIdlePooledConnection();
@@ -13,22 +13,30 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
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;
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;
private final Supplier<? extends DatabaseAgent> agentFactory;
private final Supplier<? extends SessionRpcHandler> sessionHandlerFactory;
private final Map<String, Session> sessions = new ConcurrentHashMap<>();
private final ExecutorService requests = Executors.newCachedThreadPool();
private final ExecutorService requests;
private final ExecutorService cleanup;
private final JdbcConnectionPoolRegistry poolRegistry;
private final Gson gson = new Gson();
private final PrintStream protocolOutput = System.out;
@@ -38,29 +46,43 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
private ScheduledExecutorService maintenance;
public MultiSessionJsonRpcServer(Supplier<? extends DatabaseAgent> agentFactory) {
this(agentFactory, new JdbcConnectionPoolRegistry());
this(agentFactory, new JdbcConnectionPoolRegistry(), RuntimeLimits.defaults());
}
MultiSessionJsonRpcServer(
Supplier<? extends DatabaseAgent> agentFactory,
JdbcConnectionPoolRegistry.PoolSettings poolSettings
) {
this(agentFactory, new JdbcConnectionPoolRegistry(poolSettings));
this(agentFactory, new JdbcConnectionPoolRegistry(poolSettings), RuntimeLimits.defaults());
}
MultiSessionJsonRpcServer(
Supplier<? extends DatabaseAgent> agentFactory,
JdbcConnectionPoolRegistry.PoolSettings poolSettings,
RuntimeLimits runtimeLimits
) {
this(agentFactory, new JdbcConnectionPoolRegistry(poolSettings), runtimeLimits);
}
private MultiSessionJsonRpcServer(
Supplier<? extends DatabaseAgent> agentFactory,
JdbcConnectionPoolRegistry poolRegistry
JdbcConnectionPoolRegistry poolRegistry,
RuntimeLimits runtimeLimits
) {
this.agentFactory = agentFactory;
this.sessionHandlerFactory = null;
this.poolRegistry = poolRegistry;
this.requests = boundedExecutor(runtimeLimits.maximumRequestThreads, "dbx-agent-request");
this.cleanup = boundedExecutor(runtimeLimits.maximumCleanupThreads, "dbx-agent-cleanup");
}
private MultiSessionJsonRpcServer(Supplier<? extends SessionRpcHandler> sessionHandlerFactory, boolean customHandler) {
this.agentFactory = null;
this.sessionHandlerFactory = sessionHandlerFactory;
this.poolRegistry = new JdbcConnectionPoolRegistry();
RuntimeLimits runtimeLimits = RuntimeLimits.defaults();
this.requests = boundedExecutor(runtimeLimits.maximumRequestThreads, "dbx-agent-request");
this.cleanup = boundedExecutor(runtimeLimits.maximumCleanupThreads, "dbx-agent-cleanup");
}
/** Creates a protocol v2 server for a non-JDBC, session-scoped agent. */
@@ -82,7 +104,7 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
writeResponse(handleRequest(request));
return;
}
requests.submit(() -> writeResponse(handleRequest(request)));
executeRequest(request, this::writeResponse);
}
} catch (Exception e) {
throw new RuntimeException(e);
@@ -95,6 +117,14 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
return gson.toJson(handleRequest(JsonParser.parseString(line).getAsJsonObject()));
}
void executeRequest(JsonObject request, Consumer<JsonObject> responseConsumer) {
try {
requests.execute(() -> responseConsumer.accept(handleRequest(request)));
} catch (RejectedExecutionException error) {
responseConsumer.accept(errorResponse(request.get("id"), AgentRpcError.backpressure("request", error)));
}
}
private JsonObject handleRequest(JsonObject request) {
JsonElement id = request.get("id");
String method = request.get("method").getAsString();
@@ -134,10 +164,7 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
}
response.add("result", gson.toJsonTree(result));
} catch (Throwable error) {
JsonObject rpcError = new JsonObject();
rpcError.addProperty("code", -1);
rpcError.addProperty("message", error.getMessage() == null ? error.toString() : error.getMessage());
response.add("error", rpcError);
response.add("error", AgentRpcError.toJson(error, method, stringOrNull(params, "agentSessionId")));
}
return response;
}
@@ -165,7 +192,7 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
return session.connect(params);
} catch (Exception error) {
sessions.remove(sessionId, session);
session.close();
session.quarantineAndClose(cleanup);
throw error;
}
}
@@ -173,7 +200,13 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
private Object closeSession(String sessionId) {
Session session = sessions.remove(sessionId);
if (session != null) {
session.close();
boolean replaceRuntime = session.quarantineAndClose(cleanup);
if (replaceRuntime) {
throw AgentRpcError.resource(
"close",
new IllegalStateException("JDBC quarantine operation limit reached")
);
}
}
return Collections.singletonMap("ok", true);
}
@@ -206,7 +239,11 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
private void closeAllSessions() {
for (String sessionId : sessions.keySet()) {
closeSession(sessionId);
try {
closeSession(sessionId);
} catch (AgentRpcError ignored) {
// Sessions are already detached; process shutdown remains the final cleanup boundary.
}
}
}
@@ -263,7 +300,16 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
}
}
closeAllSessions();
requests.shutdown();
requests.shutdownNow();
cleanup.shutdown();
try {
if (!cleanup.awaitTermination(2, TimeUnit.SECONDS)) {
cleanup.shutdownNow();
}
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
cleanup.shutdownNow();
}
poolRegistry.close();
}
@@ -275,6 +321,47 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
};
}
private static ExecutorService boundedExecutor(int maximumThreads, String threadName) {
return new ThreadPoolExecutor(
0,
maximumThreads,
60L,
TimeUnit.SECONDS,
new SynchronousQueue<>(),
daemonThreadFactory(threadName),
new ThreadPoolExecutor.AbortPolicy()
);
}
private static JsonObject errorResponse(JsonElement id, Throwable error) {
JsonObject response = new JsonObject();
response.addProperty("jsonrpc", "2.0");
response.add("id", id);
response.add("error", AgentRpcError.toJson(error, "request", null));
return response;
}
private static String stringOrNull(JsonObject params, String key) {
return params.has(key) && !params.get(key).isJsonNull() ? params.get(key).getAsString() : null;
}
static final class RuntimeLimits {
private final int maximumRequestThreads;
private final int maximumCleanupThreads;
RuntimeLimits(int maximumRequestThreads, int maximumCleanupThreads) {
if (maximumRequestThreads <= 0 || maximumCleanupThreads <= 0) {
throw new IllegalArgumentException("Agent runtime thread limits must be positive");
}
this.maximumRequestThreads = maximumRequestThreads;
this.maximumCleanupThreads = maximumCleanupThreads;
}
private static RuntimeLimits defaults() {
return new RuntimeLimits(MAX_REQUEST_THREADS, MAX_CLEANUP_THREADS);
}
}
private static String requiredSessionId(JsonObject params) {
if (!params.has("agentSessionId") || params.get("agentSessionId").getAsString().trim().isEmpty()) {
throw new IllegalArgumentException("agentSessionId is required");
@@ -293,6 +380,8 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
private final JsonRpcServer server;
private final SessionRpcHandler handler;
private final ReentrantLock lock = new ReentrantLock();
private final AtomicReference<State> state = new AtomicReference<>(State.ACTIVE);
private final AtomicBoolean cleanupScheduled = new AtomicBoolean();
private Session(JsonRpcServer server) {
this.server = server;
@@ -305,8 +394,10 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
}
private Object handle(String method, JsonObject params) throws Exception {
requireActive();
lock.lock();
try {
requireActive();
return handler == null ? server.dispatchForRuntime(method, params) : handler.handle(method, params);
} finally {
lock.unlock();
@@ -314,8 +405,10 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
}
private Object connect(JsonObject params) throws Exception {
requireActive();
lock.lock();
try {
requireActive();
return handler == null
? server.dispatchForRuntime(AgentProtocol.METHOD_CONNECT, params)
: handler.connect(params);
@@ -324,9 +417,31 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
}
}
private boolean quarantineAndClose(ExecutorService cleanup) {
state.compareAndSet(State.ACTIVE, State.QUARANTINED);
boolean replaceRuntime = server != null && server.quarantine();
if (!cleanupScheduled.compareAndSet(false, true)) {
return replaceRuntime;
}
try {
cleanup.execute(this::closeWhenIdle);
} catch (RejectedExecutionException error) {
throw AgentRpcError.resource("close", error);
}
return replaceRuntime;
}
private void closeWhenIdle() {
close();
}
private void close() {
state.compareAndSet(State.ACTIVE, State.QUARANTINED);
lock.lock();
try {
if (state.get() == State.CLOSED) {
return;
}
if (handler != null) {
handler.close();
} else {
@@ -334,6 +449,7 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
}
} catch (Exception ignored) {
} finally {
state.set(State.CLOSED);
lock.unlock();
}
}
@@ -347,7 +463,7 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
}
private void expireIdleResources() {
if (handler != null) {
if (state.get() != State.ACTIVE || handler != null) {
return;
}
if (!lock.tryLock()) {
@@ -361,7 +477,7 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
}
private void expireIdleResources(long nowMillis, long idleTimeoutMillis) {
if (handler != null) {
if (state.get() != State.ACTIVE || handler != null) {
return;
}
if (!lock.tryLock()) {
@@ -373,5 +489,17 @@ public final class MultiSessionJsonRpcServer implements AutoCloseable {
lock.unlock();
}
}
private void requireActive() {
if (state.get() != State.ACTIVE) {
throw new IllegalStateException("Agent session is quarantined");
}
}
private enum State {
ACTIVE,
QUARANTINED,
CLOSED
}
}
}