Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ public class ServerConfiguration {
private static final String TELEMETRY_POOL_METRICS_ENABLED_KEY = "ojp.telemetry.pool.metrics.enabled";
private static final String TELEMETRY_CACHE_METRICS_ENABLED_KEY = "ojp.telemetry.cache.metrics.enabled";

// Eager Close ResultSet mode configuration key
private static final String EAGER_CLOSE_ENABLED_KEY = "ojp.resultset.eagerClose.enabled";

// TLS configuration keys
private static final String TLS_ENABLED_KEY = "ojp.server.tls.enabled";
private static final String TLS_KEYSTORE_PATH_KEY = "ojp.server.tls.keystore.path";
Expand Down Expand Up @@ -155,6 +158,9 @@ public class ServerConfiguration {
public static final boolean DEFAULT_TELEMETRY_POOL_METRICS_ENABLED = true; // Enabled by default when OpenTelemetry is enabled
public static final boolean DEFAULT_TELEMETRY_CACHE_METRICS_ENABLED = true; // Enabled by default when OpenTelemetry is enabled

// Eager Close ResultSet mode default values
public static final boolean DEFAULT_EAGER_CLOSE_ENABLED = true; // Enabled by default

// TLS default values
public static final boolean DEFAULT_TLS_ENABLED = false; // Disabled by default for backwards compatibility
public static final boolean DEFAULT_TLS_CLIENT_AUTH_REQUIRED = false; // mTLS disabled by default
Expand Down Expand Up @@ -247,6 +253,9 @@ public class ServerConfiguration {
private final String tlsTruststoreType;
private final boolean tlsClientAuthRequired;

// Eager Close ResultSet mode configuration
private final boolean eagerCloseEnabled;

// ResultSet streaming configuration
private final int resultsetRowsPerBlock;

Expand Down Expand Up @@ -316,6 +325,9 @@ public ServerConfiguration() {
this.tlsTruststoreType = getStringProperty(TLS_TRUSTSTORE_TYPE_KEY, "JKS");
this.tlsClientAuthRequired = getBooleanProperty(TLS_CLIENT_AUTH_REQUIRED_KEY, DEFAULT_TLS_CLIENT_AUTH_REQUIRED);

// Eager Close ResultSet mode configuration
this.eagerCloseEnabled = getBooleanProperty(EAGER_CLOSE_ENABLED_KEY, DEFAULT_EAGER_CLOSE_ENABLED);

// Tracing configuration
this.tracingEnabled = getBooleanProperty(TRACING_ENABLED_KEY, DEFAULT_TRACING_ENABLED);
this.tracingEndpoint = getStringProperty(TRACING_ENDPOINT_KEY, DEFAULT_TRACING_ENDPOINT);
Expand Down Expand Up @@ -497,6 +509,8 @@ private void logConfigurationSummary() {
logger.info(" TLS Keystore Type: {}", tlsKeystoreType);
logger.info(" TLS Truststore Type: {}", tlsTruststoreType);
}
logger.info("Eager Close ResultSet Mode:");
logger.info(" Eager Close Enabled: {}", eagerCloseEnabled);
logger.info("Tracing Configuration:");
logger.info(" Tracing Enabled: {}", tracingEnabled);
if (tracingEnabled) {
Expand Down Expand Up @@ -781,6 +795,10 @@ public boolean isTelemetryCacheMetricsEnabled() {
return telemetryCacheMetricsEnabled;
}

public boolean isEagerCloseEnabled() {
return eagerCloseEnabled;
}

public int getResultsetRowsPerBlock() {
return resultsetRowsPerBlock;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ public String registerResultSet(SessionInfo sessionInfo, ResultSet rs) {

@Override
public ResultSet getResultSet(SessionInfo sessionInfo, String uuid) {
return this.sessionMap.get(sessionInfo.getSessionUUID()).getResultSet(uuid);
Session session = this.sessionMap.get(sessionInfo.getSessionUUID());
return session != null ? session.getResultSet(uuid) : null;
}

@Override
Expand All @@ -129,7 +130,8 @@ public String registerStatement(SessionInfo sessionInfo, Statement stmt) {

@Override
public Statement getStatement(SessionInfo sessionInfo, String uuid) {
return this.sessionMap.get(sessionInfo.getSessionUUID()).getStatement(uuid);
Session session = this.sessionMap.get(sessionInfo.getSessionUUID());
return session != null ? session.getStatement(uuid) : null;
}

@Override
Expand All @@ -141,7 +143,8 @@ public String registerPreparedStatement(SessionInfo sessionInfo, PreparedStateme

@Override
public PreparedStatement getPreparedStatement(SessionInfo sessionInfo, String uuid) {
return this.sessionMap.get(sessionInfo.getSessionUUID()).getPreparedStatement(uuid);
Session session = this.sessionMap.get(sessionInfo.getSessionUUID());
return session != null ? session.getPreparedStatement(uuid) : null;
}

@Override
Expand All @@ -153,7 +156,8 @@ public String registerCallableStatement(SessionInfo sessionInfo, CallableStateme

@Override
public CallableStatement getCallableStatement(SessionInfo sessionInfo, String uuid) {
return this.sessionMap.get(sessionInfo.getSessionUUID()).getCallableStatement(uuid);
Session session = this.sessionMap.get(sessionInfo.getSessionUUID());
return session != null ? session.getCallableStatement(uuid) : null;
}

@Override
Expand Down Expand Up @@ -235,13 +239,15 @@ public void waitLobStreamsConsumption(SessionInfo sessionInfo) {
@Override
public void registerAttr(SessionInfo sessionInfo, String key, Object value) {
Session session = this.sessionMap.get(sessionInfo.getSessionUUID());
session.addAttr(key, value);
if (session != null) {
session.addAttr(key, value);
}
}

@Override
public Object getAttr(SessionInfo sessionInfo, String key) {
Session session = this.sessionMap.get(sessionInfo.getSessionUUID());
return session.getAttr(key);
return session != null ? session.getAttr(key) : null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,17 @@ public void execute(ActionContext context, CallResourceRequest request, StreamOb
responseBuilder.setSession(request.getSession());
}

// If the session was eagerly terminated (e.g., after a fully-read ResultSet with no LOBs
// outside a transaction), the resource may be null. Return an empty success response so
// that close() calls from the JDBC driver do not surface as errors to the application.
if (resource == null) {
log.debug("Resource not found for session {} (session may have been eagerly closed) - returning empty success",
request.getSession().getSessionUUID());
responseObserver.onNext(responseBuilder.build());
responseObserver.onCompleted();
return;
}

List<Object> paramsReceived = (request.getTarget().getParamsCount() > 0) ?
ProtoConverter.parameterValuesToObjectList(request.getTarget().getParamsList()) : EMPTY_LIST;
Class<?> clazz = resource.getClass();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.openjproxy.grpc.server.utils.DateTimeUtils;

import java.sql.Clob;
import java.sql.Connection;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
Expand Down Expand Up @@ -97,9 +98,19 @@ public static void handleResultSet(ActionContext context, SessionInfo session, S
String resultSetMode = "";
boolean resultSetMetadataCollected = false;

boolean eagerCloseEnabled = context.getServerConfiguration().isEagerCloseEnabled();
if (eagerCloseEnabled) {
// Snapshot metadata early so it survives RS closure.
collectResultSetMetadata(context, session, resultSetUUID, rs);
resultSetMetadataCollected = true;
}

while (rs.next()) {
// DB2 requires metadata to be captured before LOBs can move the cursor;
// skip if already collected (e.g., by eager-close pre-loop snapshot above).
if (DbName.DB2.equals(dbName) && !resultSetMetadataCollected) {
collectResultSetMetadata(context, session, resultSetUUID, rs);
resultSetMetadataCollected = true;
}
justSent = false;
row++;
Expand Down Expand Up @@ -223,6 +234,50 @@ public static void handleResultSet(ActionContext context, SessionInfo session, S

responseObserver.onCompleted();

if (shouldEagerClose(context, session, dbName, resultSetMode, eagerCloseEnabled)) {
Connection connForCheck = context.getSessionManager().getConnection(session);
if (connForCheck != null && connForCheck.getAutoCommit()
&& rs.getType() == ResultSet.TYPE_FORWARD_ONLY) {
try {
rs.close();
log.debug("Eager close: closed forward-only ResultSet {} (no LOBs, auto-commit, non-XA)",
resultSetUUID);
} catch (SQLException e) {
log.debug("Eager close: error closing ResultSet: {}", e.getMessage());
}
}
}
}

/**
* Returns {@code true} when the preconditions for eagerly closing the ResultSet cursor are met.
* <p>
* All five conditions must hold:
* <ol>
* <li>The eager close feature is enabled.</li>
* <li>The database is not DB2 (DB2 requires post-iteration metadata access via a session attribute).</li>
* <li>The result set was not in row-by-row (LOB) mode for SQL Server / DB2.</li>
* <li>No LOBs or binary streams were registered in the session during row processing.</li>
* <li>The session is not an XA session (XA lifecycle is managed separately).</li>
* </ol>
* Two additional guards are applied by the caller:
* <ul>
* <li><b>auto-commit check</b> — the session must not be inside an active transaction.</li>
* <li><b>forward-only check</b> — only {@link ResultSet#TYPE_FORWARD_ONLY} result sets are
* closed early; scrollable result sets must stay open so the client can call navigation
* methods such as {@code first()}, {@code last()}, and {@code absolute()} after the stream
* completes.</li>
* </ul>
* The session and its Statement are intentionally left alive after the RS cursor is freed so that
* the JDBC connection can be reused for subsequent statements without interruption.
*/
private static boolean shouldEagerClose(ActionContext context, SessionInfo session, DbName dbName,
String resultSetMode, boolean eagerCloseEnabled) {
return eagerCloseEnabled
&& !DbName.DB2.equals(dbName)
&& resultSetMode.isEmpty()
&& context.getSessionManager().getLobs(session).isEmpty()
&& !session.getIsXA();
}

/**
Expand Down
Loading