-
Notifications
You must be signed in to change notification settings - Fork 39
Fix ambiguous errors when statements are cancelled or connection closed #1291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8774985
5a5605f
4f9c266
c37ed02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package com.databricks.jdbc.dbclient.impl.sqlexec; | ||
|
|
||
| import static com.databricks.jdbc.common.DatabricksJdbcConstants.JSON_HTTP_HEADERS; | ||
| import static com.databricks.jdbc.common.DatabricksJdbcConstants.OPERATION_CANCELLED_SQLSTATE; | ||
| import static com.databricks.jdbc.common.DatabricksJdbcConstants.QUERY_EXECUTION_TIMEOUT_SQLSTATE; | ||
| import static com.databricks.jdbc.common.DatabricksJdbcConstants.TEMPORARY_REDIRECT_STATUS_CODE; | ||
| import static com.databricks.jdbc.common.EnvironmentVariables.DEFAULT_RESULT_ROW_LIMIT; | ||
|
|
@@ -709,6 +710,17 @@ void handleFailedExecution( | |
| ExecuteStatementResponse response, String statementId, String statement) throws SQLException { | ||
| StatementState statementState = response.getStatus().getState(); | ||
| ServiceError error = response.getStatus().getError(); | ||
|
|
||
| // Distinguish cancellation from failure | ||
| if (statementState == StatementState.CANCELED) { | ||
| String cancelMessage = String.format("Statement [%s] was cancelled", statementId); | ||
| LOGGER.info(cancelMessage); | ||
| throw new DatabricksSQLException( | ||
| cancelMessage, | ||
| OPERATION_CANCELLED_SQLSTATE, | ||
| DatabricksDriverErrorCode.EXECUTE_STATEMENT_CANCELLED); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [F2] The PR's customer-impact section promises:
But the 3-arg public DatabricksSQLException(
String reason, String sqlState, DatabricksDriverErrorCode internalError) {
super(reason, sqlState); // ← internalError NOT passed to vendor code
logTelemetryEvent(sqlState, reason, false); // ← internalError NOT logged either
}The Suggested fix: either (a) fix the constructor to use |
||
| } | ||
|
|
||
| String errorMessage = | ||
| String.format( | ||
| "Statement execution failed %s -> %s\n%s.", statementId, statement, statementState); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| package com.databricks.jdbc.dbclient.impl.thrift; | ||
|
|
||
| import static com.databricks.jdbc.common.DatabricksJdbcConstants.OPERATION_CANCELLED_SQLSTATE; | ||
| import static com.databricks.jdbc.common.DatabricksJdbcConstants.QUERY_EXECUTION_TIMEOUT_SQLSTATE; | ||
| import static com.databricks.jdbc.common.EnvironmentVariables.*; | ||
| import static com.databricks.jdbc.common.util.DatabricksThriftUtil.*; | ||
|
|
@@ -422,6 +423,9 @@ DatabricksResultSet getStatementResult( | |
| try { | ||
| response = getOperationStatus(request, statementId); | ||
| TOperationState operationState = response.getOperationState(); | ||
| if (operationState == TOperationState.CANCELED_STATE) { | ||
| throw cancelledStatementException(statementId.toSQLExecStatementId()); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [F4] Asymmetric fix — SEA This Thrift fix is correct, but the SEA equivalent at public DatabricksResultSet getStatementResult(...) throws SQLException {
...
response = apiClient.execute(req, GetStatementResponse.class);
...
return new DatabricksResultSet(
response.getStatus(),
typedStatementId,
response.getResult(), // ← null when state is CANCELED
...);
}A user who calls Suggested fix: after the |
||
| } | ||
| if (operationState == TOperationState.FINISHED_STATE) { | ||
| verifySuccessStatus( | ||
| response.getStatus(), "getStatementResult", statementId.toSQLExecStatementId()); | ||
|
|
@@ -828,6 +832,11 @@ private void checkOperationStatusForErrors(TGetOperationStatusResp statusResp, S | |
| errorMsg, statusResp.isSetSqlState() ? statusResp.getSqlState() : null); | ||
| } | ||
|
|
||
| if (statusResp.isSetOperationState() | ||
| && statusResp.getOperationState() == TOperationState.CANCELED_STATE) { | ||
| throw cancelledStatementException(statementId); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [F5] New CANCELED branch in This branch is on the synchronous polling path — it is reached from The PR's only Thrift test ( Suggested fix: add a test where |
||
| } | ||
|
|
||
| if (statusResp.isSetOperationState() && isErrorOperationState(statusResp.getOperationState())) { | ||
| String errorMsg = | ||
| String.format( | ||
|
|
@@ -863,6 +872,13 @@ private <T extends TBase<T, F>, F extends TFieldIdEnum> boolean hasResultDataInD | |
| return directResults.isSetResultSet() && directResults.isSetResultSetMetadata(); | ||
| } | ||
|
|
||
| private DatabricksSQLException cancelledStatementException(String statementId) { | ||
| String msg = String.format("Statement [%s] was cancelled", statementId); | ||
| LOGGER.info(msg); | ||
| return new DatabricksSQLException( | ||
| msg, OPERATION_CANCELLED_SQLSTATE, DatabricksDriverErrorCode.EXECUTE_STATEMENT_CANCELLED); | ||
| } | ||
|
|
||
| private boolean isErrorStatusCode(TStatus status) { | ||
| if (status == null || !status.isSetStatusCode()) { | ||
| LOGGER.error("Status code is not set, marking the response as failed"); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package com.databricks.jdbc.api.impl; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.*; | ||
|
|
||
| import com.databricks.jdbc.model.core.StatementStatus; | ||
| import com.databricks.sdk.service.sql.StatementState; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class ExecutionStatusTest { | ||
|
|
||
| @Test | ||
| void cancelledStateWithNullError_hasMeaningfulMessage() { | ||
| StatementStatus cancelledStatus = | ||
| new StatementStatus().setState(StatementState.CANCELED).setError(null); | ||
| ExecutionStatus status = new ExecutionStatus(cancelledStatus); | ||
| assertNotNull(status.getErrorMessage(), "Cancelled state should have non-null error message"); | ||
| assertTrue(status.getErrorMessage().contains("cancelled")); | ||
| } | ||
|
|
||
| @Test | ||
| void failedStateWithNullError_hasNullMessage() { | ||
| StatementStatus failedStatus = | ||
| new StatementStatus().setState(StatementState.FAILED).setError(null); | ||
| ExecutionStatus status = new ExecutionStatus(failedStatus); | ||
| assertNull(status.getErrorMessage(), "Failed state with null error should have null message"); | ||
| } | ||
|
|
||
| @Test | ||
| void succeededStateWithNullError_hasNullMessage() { | ||
| StatementStatus succeededStatus = | ||
| new StatementStatus().setState(StatementState.SUCCEEDED).setError(null); | ||
| ExecutionStatus status = new ExecutionStatus(succeededStatus); | ||
| assertNull(status.getErrorMessage()); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[F3] Cancellation emits ERROR-level telemetry — defeats the PR's "distinguishability" goal — High
This
throw(and the symmetric one inDatabricksThriftAccessor.java:859) goes throughDatabricksSQLException(reason, sqlState, internalError), which calls:logTelemetryEventwithsilentExceptions=falseunconditionally callsexportFailureLog(..., TelemetryLogLevel.ERROR)(DatabricksSQLException.java:73-79). So every user-initiatedStatement.cancel()and every Ctrl-C from a BI tool emits an ERROR-level telemetry event withsqlState=HY008.Cancellations are common in normal BI workloads (Tableau/Looker speculatively cancel; users close tabs); a customer batch-cancelling 10k queries will look like a 10k-error spike to driver-side monitoring. The PR's goal — making cancellations distinguishable from real failures — is undone by sending them through the same ERROR alert path as real failures.
Suggested fix: introduce a 3-arg
silentExceptions=trueoverload (or directly downgrade toINFO/TRACEinsidelogTelemetryEventwhensqlState == OPERATION_CANCELLED_SQLSTATE), and use it for both cancellation throw sites. Bonus: this also helps F4-style idempotency (concurrent pollers each observing CANCELED would no longer multi-fire ERROR telemetry).