Skip to content

Commit 3e7a206

Browse files
authored
fix(bigquery-jdbc): fix Exception handling (#14566)
- Restore the thread's interrupt flag (`Thread.currentThread().interrupt()`) on every `InterruptedException` catch in Connection, Statement, PreparedStatement, ResultSet, DatabaseMetaData and OpenTelemetry tracing. - Throw checked `BigQueryJdbcException` instead of unchecked `BigQueryJdbcRuntimeException` from public JDBC methods (`execute*`, `executeBatch`, `getMoreResults`, `next`, `commit`, `rollback`, `close`, Write API batch), as JDBC requires `SQLException`. - Replace `IllegalStateException` with `BigQueryJdbcException` in `checkClosed()`, `checkIfEnabledSession()`, and `commit()`/`rollback()` when no transaction is active. Methods that call these checks (`getDefaultDataset`, `getLocation`, `getAuthProperties`, `getAutoCommit`, Statement constructors) now declare `throws SQLException`. - Wrap background fetch errors once: worker threads put the raw cause in the buffer, and `ResultSet.next()` wraps it a single time via `backgroundFetchError()`, keeping the `BigQueryException` cause so its error details stay available. - Harden `Connection.close()`: errors from `abortSession()` and interrupts while waiting for executors to shut down are collected as suppressed exceptions and rethrown as `BigQueryJdbcException`. `isClosed` is now set even when close throws. - Remove `throws InterruptedException` from `runQuery()`, which also removes the duplicate catch blocks in its callers. - Stop the GC daemon polling thread from crashing on interrupt; it now logs the exception with its stack trace and restores the interrupt flag. - `withTracing` no longer turns checked exceptions into unchecked ones. - Remove the `BigQueryJdbcRuntimeException(String, InterruptedException)` constructor. - Update unit and integration tests to expect `SQLException` / `BigQueryJdbcException`.
1 parent 75cb0a8 commit 3e7a206

14 files changed

Lines changed: 149 additions & 151 deletions

‎java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/exception/BigQueryJdbcException.java‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,22 @@ public BigQueryJdbcException(String message, Throwable cause) {
6262
}
6363

6464
/**
65-
* Constructs a new BigQueryJdbcException with the specified cause and a detail message of
66-
* (cause==null ? null : cause.toString())
65+
* Constructs a new BigQueryJdbcException with the specified cause and a detail message of {@code
66+
* cause.toString()}. If {@code cause} is already a BigQueryJdbcException it is copied rather than
67+
* wrapped, so re-throwing through this constructor does not nest the exception a second time.
6768
*
6869
* @param cause Throwable that is being converted.
6970
*/
7071
public BigQueryJdbcException(Throwable cause) {
71-
super(cause);
72+
super(
73+
cause instanceof BigQueryJdbcException ? cause.getMessage() : String.valueOf(cause),
74+
cause instanceof SQLException ? ((SQLException) cause).getSQLState() : null,
75+
cause instanceof SQLException ? ((SQLException) cause).getErrorCode() : 0,
76+
cause instanceof BigQueryJdbcException ? cause.getCause() : cause);
77+
if (cause instanceof BigQueryJdbcException) {
78+
bigQueryException = ((BigQueryJdbcException) cause).bigQueryException;
79+
setStackTrace(cause.getStackTrace());
80+
}
7281
}
7382

7483
public BigQueryException getBigQueryException() {

‎java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/exception/BigQueryJdbcRuntimeException.java‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,6 @@ public BigQueryJdbcRuntimeException(Throwable ex) {
3636
super(ex);
3737
}
3838

39-
/**
40-
* Constructs a new BigQueryJdbcRuntimeException from a Throwable exception and a message.
41-
*
42-
* @param message The detail message.
43-
* @param ex Throwable to be thrown.
44-
*/
45-
public BigQueryJdbcRuntimeException(String message, InterruptedException ex) {
46-
super(message, ex);
47-
}
48-
4939
public BigQueryJdbcRuntimeException(String message, Throwable ex) {
5040
super(BigQueryJdbcExceptionUtils.formatMessage(message, ex), ex);
5141
}

‎java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryArrowResultSet.java‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import com.google.cloud.bigquery.Schema;
2626
import com.google.cloud.bigquery.StandardSQLTypeName;
2727
import com.google.cloud.bigquery.exception.BigQueryJdbcException;
28-
import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException;
2928
import com.google.cloud.bigquery.storage.v1.ArrowRecordBatch;
3029
import com.google.cloud.bigquery.storage.v1.ArrowSchema;
3130
import io.opentelemetry.context.Scope;
@@ -280,7 +279,7 @@ public boolean next() throws SQLException {
280279
try (Scope scope = makeOriginalContextCurrent()) {
281280
BigQueryArrowBatchWrapper batchWrapper = this.buffer.take();
282281
if (batchWrapper.getException() != null) {
283-
throw new BigQueryJdbcRuntimeException(batchWrapper.getException());
282+
throw backgroundFetchError(batchWrapper.getException());
284283
}
285284
if (batchWrapper.isLast()) {
286285
/* Marks the end of the records */
@@ -309,10 +308,15 @@ else if (this.currentBatchRowIndex < this.vectorSchemaRoot.getRowCount()) {
309308
this.rowCount++;
310309
return true;
311310
}
312-
} catch (InterruptedException | SQLException ex) {
311+
} catch (SQLException ex) {
313312
throw new BigQueryJdbcException(
314313
"Error occurred while advancing the cursor. This could happen when connection is closed while the next method is being called.",
315314
ex);
315+
} catch (InterruptedException ex) {
316+
Thread.currentThread().interrupt();
317+
throw new BigQueryJdbcException(
318+
"Interrupted while advancing the cursor. This could happen when connection is closed while the next method is being called.",
319+
ex);
316320
}
317321
}
318322
return false;

‎java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseResultSet.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,4 +843,11 @@ public void clearWarnings() throws SQLException {
843843
this.warnings = null;
844844
this.warningsLoaded = true;
845845
}
846+
847+
static BigQueryJdbcException backgroundFetchError(Exception cause) {
848+
String msg = "Error occurred while fetching query results";
849+
return cause instanceof BigQueryException
850+
? new BigQueryJdbcException(msg, (BigQueryException) cause)
851+
: new BigQueryJdbcException(msg, cause);
852+
}
846853
}

‎java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java‎

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ public PreparedStatement prepareStatement(String sql, int resultSetType, int res
592592
return prepareStatement(sql);
593593
}
594594

595-
public DatasetId getDefaultDataset() {
595+
public DatasetId getDefaultDataset() throws BigQueryJdbcException {
596596
checkClosed();
597597
return this.defaultDataset;
598598
}
@@ -613,12 +613,12 @@ String getKmsKeyName() {
613613
return this.kmsKeyName;
614614
}
615615

616-
public String getLocation() {
616+
public String getLocation() throws BigQueryJdbcException {
617617
checkClosed();
618618
return this.location;
619619
}
620620

621-
public Map<String, String> getAuthProperties() {
621+
public Map<String, String> getAuthProperties() throws BigQueryJdbcException {
622622
checkClosed();
623623
return this.authProperties;
624624
}
@@ -678,7 +678,7 @@ SessionState getSessionStateSnapshot() {
678678
* For more information about transactions in BigQuery, see <a
679679
* href="https://cloud.google.com/bigquery/docs/transactions">Multi-statement transactions</a>.
680680
*/
681-
private void beginTransaction() {
681+
private void beginTransaction() throws BigQueryJdbcException {
682682
LOG.finer("++enter++");
683683
SessionState snapshot = this.sessionState.get();
684684
QueryJobConfiguration.Builder transactionBeginJobConfig =
@@ -696,7 +696,8 @@ private void beginTransaction() {
696696
initSessionInfo(transactionResult.getSessionInfo().getSessionId());
697697
this.transactionStarted = true;
698698
} catch (InterruptedException ex) {
699-
throw new BigQueryJdbcRuntimeException("Failed to begin transaction", ex);
699+
Thread.currentThread().interrupt();
700+
throw new BigQueryJdbcException("Failed to begin transaction", ex);
700701
}
701702
}
702703

@@ -930,7 +931,7 @@ public void clearWarnings() {
930931
}
931932

932933
@Override
933-
public boolean getAutoCommit() {
934+
public boolean getAutoCommit() throws SQLException {
934935
checkClosed();
935936
return this.autoCommit;
936937
}
@@ -954,16 +955,13 @@ public void setAutoCommit(boolean autoCommit) throws SQLException {
954955
}
955956

956957
@Override
957-
public void commit() {
958+
public void commit() throws SQLException {
958959
checkClosed();
959960
checkIfEnabledSession("commit");
960961
if (!isTransactionStarted()) {
961-
IllegalStateException ex =
962-
new IllegalStateException(
963-
"Cannot commit without an active transaction. Please set setAutoCommit to false to start"
964-
+ " a transaction.");
965-
LOG.severe(ex.getMessage(), ex);
966-
throw ex;
962+
throw new BigQueryJdbcException(
963+
"Cannot commit without an active transaction. Please set setAutoCommit to false to start"
964+
+ " a transaction.");
967965
}
968966
commitTransaction();
969967
if (!getAutoCommit()) {
@@ -976,12 +974,9 @@ public void rollback() throws SQLException {
976974
checkClosed();
977975
checkIfEnabledSession("rollback");
978976
if (!isTransactionStarted()) {
979-
IllegalStateException ex =
980-
new IllegalStateException(
981-
"Cannot rollback without an active transaction. Please set setAutoCommit to false to"
982-
+ " start a transaction.");
983-
LOG.severe(ex.getMessage(), ex);
984-
throw ex;
977+
throw new BigQueryJdbcException(
978+
"Cannot rollback without an active transaction. Please set setAutoCommit to false to"
979+
+ " start a transaction.");
985980
}
986981
rollbackImpl();
987982
}
@@ -999,8 +994,11 @@ private void rollbackImpl() throws SQLException {
999994
if (!getAutoCommit()) {
1000995
beginTransaction();
1001996
}
1002-
} catch (InterruptedException | BigQueryException ex) {
997+
} catch (BigQueryException ex) {
1003998
throw new BigQueryJdbcException("Failed to rollback transaction", ex);
999+
} catch (InterruptedException ex) {
1000+
Thread.currentThread().interrupt();
1001+
throw new BigQueryJdbcException("Interrupted during rollback transaction", ex);
10041002
}
10051003
}
10061004

@@ -1103,10 +1101,19 @@ private void closeImpl() throws SQLException {
11031101

11041102
SessionState snapshot = this.sessionState.get();
11051103
if (snapshot.sessionInfo != null && snapshot.createdByDriver) {
1106-
abortSession(snapshot);
1104+
try {
1105+
abortSession(snapshot);
1106+
} catch (SQLException e) {
1107+
if (exceptionToThrow == null) {
1108+
exceptionToThrow = e;
1109+
} else {
1110+
exceptionToThrow.addSuppressed(e);
1111+
}
1112+
}
11071113
}
11081114

11091115
boolean interrupted = Thread.currentThread().isInterrupted();
1116+
InterruptedException terminationInterruptedException = null;
11101117

11111118
try {
11121119
if (this.bigQueryReadClient != null) {
@@ -1135,6 +1142,7 @@ private void closeImpl() throws SQLException {
11351142
}
11361143
} catch (InterruptedException e) {
11371144
interrupted = true;
1145+
terminationInterruptedException = e;
11381146
if (this.bigQueryReadClient != null) {
11391147
this.bigQueryReadClient.shutdownNow();
11401148
}
@@ -1161,23 +1169,31 @@ private void closeImpl() throws SQLException {
11611169

11621170
if (interrupted) {
11631171
Thread.currentThread().interrupt();
1164-
throw new InterruptedException("Interrupted awaiting executor termination");
1172+
}
1173+
if (terminationInterruptedException != null) {
1174+
BigQueryJdbcException interruptedException =
1175+
new BigQueryJdbcException("Interrupted during close", terminationInterruptedException);
1176+
if (exceptionToThrow == null) {
1177+
exceptionToThrow = interruptedException;
1178+
} else {
1179+
exceptionToThrow.addSuppressed(interruptedException);
1180+
}
11651181
}
11661182
} catch (ConcurrentModificationException ex) {
11671183
throw new BigQueryJdbcException("Concurrent modification during close", ex);
1168-
} catch (InterruptedException e) {
1169-
throw new BigQueryJdbcRuntimeException("Interrupted during close", e);
11701184
} finally {
11711185
BigQueryJdbcMdc.clear();
11721186
BigQueryJdbcRootLogger.closeConnectionHandler(this.connectionId);
11731187
BigQueryJdbcOpenTelemetry.unregisterConnection(this.connectionId);
11741188
BigQueryJdbcOpenTelemetry.releaseSdk(this.openTelemetry);
11751189
this.openTelemetry = null;
11761190
}
1191+
1192+
this.isClosed = true;
1193+
11771194
if (exceptionToThrow != null) {
11781195
throw exceptionToThrow;
11791196
}
1180-
this.isClosed = true;
11811197
}
11821198

11831199
ExecutorService getExecutorService() {
@@ -1193,22 +1209,16 @@ public boolean isClosed() {
11931209
return this.isClosed;
11941210
}
11951211

1196-
private void checkClosed() {
1212+
private void checkClosed() throws BigQueryJdbcException {
11971213
if (isClosed()) {
1198-
IllegalStateException ex =
1199-
new IllegalStateException("This " + getClass().getName() + " has been closed");
1200-
LOG.severe(ex.getMessage(), ex);
1201-
throw ex;
1214+
throw new BigQueryJdbcException("This " + getClass().getName() + " has been closed");
12021215
}
12031216
}
12041217

1205-
private void checkIfEnabledSession(String methodName) {
1218+
private void checkIfEnabledSession(String methodName) throws BigQueryJdbcException {
12061219
if (!this.enableSession) {
1207-
IllegalStateException ex =
1208-
new IllegalStateException(
1209-
String.format("Session needs to be enabled to use %s method.", methodName));
1210-
LOG.severe(ex.getMessage(), ex);
1211-
throw ex;
1220+
throw new BigQueryJdbcException(
1221+
String.format("Session needs to be enabled to use %s method.", methodName));
12121222
}
12131223
}
12141224

@@ -1519,7 +1529,7 @@ RetrySettings getRetrySettings() {
15191529
return retrySettingsBuilder == null ? null : retrySettingsBuilder.build();
15201530
}
15211531

1522-
private void commitTransaction() {
1532+
private void commitTransaction() throws BigQueryJdbcException {
15231533
try {
15241534
QueryJobConfiguration transactionCommitJobConfig =
15251535
QueryJobConfiguration.newBuilder("COMMIT TRANSACTION;")
@@ -1530,11 +1540,12 @@ private void commitTransaction() {
15301540
commitJob.waitFor();
15311541
this.transactionStarted = false;
15321542
} catch (InterruptedException ex) {
1533-
throw new BigQueryJdbcRuntimeException("Interrupted during commitTransaction", ex);
1543+
Thread.currentThread().interrupt();
1544+
throw new BigQueryJdbcException("Interrupted during commitTransaction", ex);
15341545
}
15351546
}
15361547

1537-
private void abortSession(SessionState snapshot) {
1548+
private void abortSession(SessionState snapshot) throws BigQueryJdbcException {
15381549
try {
15391550
LOG.fine("Aborting session on connection close: %s", snapshot.sessionInfo.getValue());
15401551
QueryJobConfiguration abortSessionJobConfig =
@@ -1544,7 +1555,7 @@ private void abortSession(SessionState snapshot) {
15441555
this.bigQuery.query(abortSessionJobConfig);
15451556
} catch (InterruptedException ex) {
15461557
Thread.currentThread().interrupt();
1547-
throw new BigQueryJdbcRuntimeException("Interrupted during session abort", ex);
1558+
throw new BigQueryJdbcException("Interrupted during session abort", ex);
15481559
} catch (BigQueryException ex) {
15491560
LOG.warning(
15501561
"Failed to abort session during session abort (session may have already ended): "

‎java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDaemonPollingTask.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ else if (referenceQueueJsonRs != null) {
118118
throw new BigQueryJdbcRuntimeException("Null Reference Queue");
119119
}
120120
} catch (InterruptedException ex) {
121-
throw new BigQueryJdbcRuntimeException("Interrupted in GC daemon task", ex);
121+
LOG.warning(ex, "Interrupted in GC daemon task");
122122
}
123123
}
124124
}

‎java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3929,7 +3929,8 @@ public boolean isWrapperFor(Class<?> iface) throws SQLException {
39293929
* for querying BigQuery's metadata.
39303930
* @see BigQueryConnection#isFilterTablesOnDefaultDataset()
39313931
*/
3932-
Tuple<String, String> determineEffectiveCatalogAndSchema(String catalog, String schemaPattern) {
3932+
Tuple<String, String> determineEffectiveCatalogAndSchema(String catalog, String schemaPattern)
3933+
throws BigQueryJdbcException {
39333934
String effectiveCatalog = catalog;
39343935
String effectiveSchemaPattern = schemaPattern;
39353936

@@ -4279,7 +4280,7 @@ private void waitForTasksCompletion(List<Future<?>> taskFutures)
42794280
Thread.currentThread().interrupt();
42804281
LOG.warning(
42814282
"Fetcher thread interrupted while waiting for tasks. Attempting to cancel remaining tasks.");
4282-
throw new SQLException("Metadata fetch interrupted.", e);
4283+
throw new BigQueryJdbcException("Metadata fetch interrupted.", e);
42834284
} finally {
42844285
for (Future<?> future : taskFutures) {
42854286
if (!future.isDone()) {
@@ -4298,6 +4299,9 @@ private BlockingQueue<BigQueryFieldValueListWrapper> fetchAndPopulateQueue(
42984299
populateQueue(collectedResults, queue, resultSchemaFields);
42994300
signalEndOfData(queue, resultSchemaFields);
43004301
return queue;
4302+
} catch (InterruptedException e) {
4303+
Thread.currentThread().interrupt();
4304+
throw new BigQueryJdbcException("Error during metadata fetch", e);
43014305
} catch (Exception e) {
43024306
if (e instanceof SQLException) {
43034307
throw (SQLException) e;
@@ -4358,7 +4362,7 @@ private List<Dataset> fetchDatasetsForProject(
43584362
return datasets != null ? datasets : Collections.emptyList();
43594363
} catch (InterruptedException e) {
43604364
Thread.currentThread().interrupt();
4361-
throw new SQLException(
4365+
throw new BigQueryJdbcException(
43624366
"Interrupted while fetching matching datasets for project " + project, e);
43634367
} catch (Exception e) {
43644368
throw new SQLException("Failed to fetch matching datasets for project " + project, e);
@@ -4667,7 +4671,7 @@ private void processTargetRoutinesConcurrently(
46674671
throw new SQLException("Error while fetching metadata", e);
46684672
} catch (InterruptedException e) {
46694673
Thread.currentThread().interrupt();
4670-
throw new SQLException("Interrupted while processing routines.", e);
4674+
throw new BigQueryJdbcException("Interrupted while processing routines.", e);
46714675
} finally {
46724676
activeFutures.forEach(future -> future.cancel(true));
46734677
}
@@ -4796,7 +4800,7 @@ private void processTargetTablesConcurrently(
47964800
throw new SQLException("Error while fetching metadata", e);
47974801
} catch (InterruptedException e) {
47984802
Thread.currentThread().interrupt();
4799-
throw new SQLException("Interrupted while processing tables.", e);
4803+
throw new BigQueryJdbcException("Interrupted while processing tables.", e);
48004804
} finally {
48014805
activeFutures.forEach(future -> future.cancel(true));
48024806
}

0 commit comments

Comments
 (0)