From 7a555d4e64c4ef2393ac1f218d064b916e075e2c Mon Sep 17 00:00:00 2001 From: Jin Seop Kim Date: Fri, 18 Sep 2026 17:08:53 -0400 Subject: [PATCH 1/3] feat(bigquery): make BigQuery AutoCloseable with default no-op close method --- .../com/google/cloud/bigquery/BigQuery.java | 12 +++- .../google/cloud/bigquery/BigQueryImpl.java | 33 ++++++++++ .../cloud/bigquery/BigQueryImplTest.java | 66 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQuery.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQuery.java index 7ca564912c43..9766e8911a07 100644 --- a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQuery.java +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQuery.java @@ -40,7 +40,7 @@ * * @see Google Cloud BigQuery */ -public interface BigQuery extends Service { +public interface BigQuery extends Service, AutoCloseable { /** * Fields of a BigQuery Dataset resource. @@ -1821,4 +1821,14 @@ Object queryWithTimeout( * represents the subset of granted permissions. */ List testIamPermissions(TableId table, List permissions, IAMOption... options); + + /** + * Closes any background resources and transport channels held by this service. + * + *

The default implementation does nothing. Implementations that manage background resources + * (such as gRPC channels or storage clients) should override this method to release them + * deterministically. + */ + @Override + default void close() {} } diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java index 44d951fdb9cf..06494ac05e20 100644 --- a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java @@ -504,6 +504,7 @@ private void closeClient() { private transient ConcurrentHashMap bqReadClients; private transient boolean isGlobalClientUserProvided; + private boolean closed = false; /** * Lazily creates or retrieves the shared {@link BigQueryReadClient} instance used for streaming @@ -527,6 +528,9 @@ BigQueryReadClient getBigQueryReadClient() { * @throws BigQueryException if initializing the storage read client fails */ BigQueryReadClient getBigQueryReadClient(String location) { + if (closed) { + throw new IllegalStateException("BigQuery service has been closed"); + } String cacheKey = location != null ? location.toLowerCase() : "global"; if (bqReadClients == null) { synchronized (this) { @@ -543,6 +547,9 @@ BigQueryReadClient getBigQueryReadClient(String location) { return client; } synchronized (this) { + if (closed) { + throw new IllegalStateException("BigQuery service has been closed"); + } client = bqReadClients.get(cacheKey); if (client == null && isGlobalClientUserProvided) { client = bqReadClients.get("global"); @@ -583,6 +590,32 @@ void setBigQueryReadClient(String location, BigQueryReadClient client) { } } + /** + * Closes any background resources and transport channels held by this {@link BigQueryImpl}, + * including the underlying {@link BigQueryReadClient} instances used for Arrow query streaming. + */ + @Override + public void close() { + List clientsToClose = new ArrayList<>(); + synchronized (this) { + if (closed) { + return; + } + closed = true; + if (bqReadClients != null) { + clientsToClose.addAll(bqReadClients.values()); + bqReadClients.clear(); + } + } + for (BigQueryReadClient client : clientsToClose) { + try { + client.close(); + } catch (Exception e) { + // Ignore exceptions during teardown + } + } + } + /** * Configures a {@link BigQueryReadSettings.Builder} with credentials, header provider, and * universe domain mapped from the given {@link BigQueryOptions}. diff --git a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java index 79028a7c8140..5a90822cb25b 100644 --- a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java +++ b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java @@ -4260,4 +4260,70 @@ void testTestIamPermissionsWhenNoPermissionsGranted() throws IOException { .testIamPermissionsSkipExceptionTranslation( resourceId, checkedPermissions, EMPTY_RPC_OPTIONS); } + + @Test + void testCloseClosesBigQueryReadClient() { + BigQueryReadClient mockReadClient = + mock(BigQueryReadClient.class, withSettings().withoutAnnotations()); + bigquery = options.getService(); + ((BigQueryImpl) bigquery).setBigQueryReadClient(mockReadClient); + + bigquery.close(); + + verify(mockReadClient, times(1)).close(); + } + + @Test + void testCloseIsIdempotent() { + BigQueryReadClient mockReadClient = + mock(BigQueryReadClient.class, withSettings().withoutAnnotations()); + bigquery = options.getService(); + ((BigQueryImpl) bigquery).setBigQueryReadClient(mockReadClient); + + bigquery.close(); + bigquery.close(); + + verify(mockReadClient, times(1)).close(); + } + + @Test + void testCloseWithoutReadClientDoesNotThrow() { + bigquery = options.getService(); + bigquery.close(); + } + + @Test + void testTryWithResources() { + BigQueryReadClient mockReadClient = + mock(BigQueryReadClient.class, withSettings().withoutAnnotations()); + try (BigQuery bq = options.getService()) { + ((BigQueryImpl) bq).setBigQueryReadClient(mockReadClient); + assertNotNull(bq); + } + verify(mockReadClient, times(1)).close(); + } + + @Test + void testGetBigQueryReadClientAfterCloseThrows() { + bigquery = options.getService(); + bigquery.close(); + assertThrows( + IllegalStateException.class, () -> ((BigQueryImpl) bigquery).getBigQueryReadClient()); + } + + @Test + void testCloseClosesAllRegionalBigQueryReadClients() { + BigQueryReadClient mockReadClientUs = + mock(BigQueryReadClient.class, withSettings().withoutAnnotations()); + BigQueryReadClient mockReadClientEu = + mock(BigQueryReadClient.class, withSettings().withoutAnnotations()); + bigquery = options.getService(); + ((BigQueryImpl) bigquery).setBigQueryReadClient("us", mockReadClientUs); + ((BigQueryImpl) bigquery).setBigQueryReadClient("eu", mockReadClientEu); + + bigquery.close(); + + verify(mockReadClientUs, times(1)).close(); + verify(mockReadClientEu, times(1)).close(); + } } From b53dc8591490439a869fccd45f2b41ea1a33eabc Mon Sep 17 00:00:00 2001 From: Jin Seop Kim Date: Fri, 18 Sep 2026 17:13:07 -0400 Subject: [PATCH 2/3] fix(bigquery): declare closed as transient volatile in BigQueryImpl --- .../src/main/java/com/google/cloud/bigquery/BigQueryImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java index 06494ac05e20..b8037c717388 100644 --- a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java @@ -504,7 +504,7 @@ private void closeClient() { private transient ConcurrentHashMap bqReadClients; private transient boolean isGlobalClientUserProvided; - private boolean closed = false; + private transient volatile boolean closed = false; /** * Lazily creates or retrieves the shared {@link BigQueryReadClient} instance used for streaming From 861ba4bc27611c8bf7251f8f05abc551ffdf8a6b Mon Sep 17 00:00:00 2001 From: Jin Seop Kim Date: Fri, 18 Sep 2026 18:22:08 -0400 Subject: [PATCH 3/3] fix(bigquery): narrow mutex in close() to closed flag and shutdown clients directly --- .../google/cloud/bigquery/BigQueryImpl.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java index b8037c717388..2d24e7bb82c1 100644 --- a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java @@ -559,6 +559,10 @@ BigQueryReadClient getBigQueryReadClient(String location) { configureReadSettings(settingsBuilder, getOptions()); try { client = BigQueryReadClient.create(settingsBuilder.build()); + if (closed) { + client.close(); + throw new IllegalStateException("BigQuery service has been closed"); + } if (bqReadClients.size() < MAX_CACHED_READ_CLIENTS) { bqReadClients.put(cacheKey, client); } @@ -596,23 +600,21 @@ void setBigQueryReadClient(String location, BigQueryReadClient client) { */ @Override public void close() { - List clientsToClose = new ArrayList<>(); synchronized (this) { if (closed) { return; } closed = true; - if (bqReadClients != null) { - clientsToClose.addAll(bqReadClients.values()); - bqReadClients.clear(); - } } - for (BigQueryReadClient client : clientsToClose) { - try { - client.close(); - } catch (Exception e) { - // Ignore exceptions during teardown + if (bqReadClients != null) { + for (BigQueryReadClient client : bqReadClients.values()) { + try { + client.close(); + } catch (Exception e) { + // Ignore exceptions during teardown + } } + bqReadClients.clear(); } }