Skip to content

Commit 00bf3de

Browse files
authored
feat(bigquery): make BigQuery AutoCloseable with default no-op close method (#14434)
Extend AutoCloseable on BigQuery with a default no-op close() method to allow deterministic resource management without breaking backwards compatibility.
1 parent 72db4e6 commit 00bf3de

3 files changed

Lines changed: 112 additions & 1 deletion

File tree

java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQuery.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
*
4141
* @see <a href="https://cloud.google.com/bigquery/what-is-bigquery">Google Cloud BigQuery</a>
4242
*/
43-
public interface BigQuery extends Service<BigQueryOptions> {
43+
public interface BigQuery extends Service<BigQueryOptions>, AutoCloseable {
4444

4545
/**
4646
* Fields of a BigQuery Dataset resource.
@@ -1821,4 +1821,14 @@ Object queryWithTimeout(
18211821
* represents the subset of granted permissions.
18221822
*/
18231823
List<String> testIamPermissions(TableId table, List<String> permissions, IAMOption... options);
1824+
1825+
/**
1826+
* Closes any background resources and transport channels held by this service.
1827+
*
1828+
* <p>The default implementation does nothing. Implementations that manage background resources
1829+
* (such as gRPC channels or storage clients) should override this method to release them
1830+
* deterministically.
1831+
*/
1832+
@Override
1833+
default void close() {}
18241834
}

java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,7 @@ private void closeClient() {
504504

505505
private transient ConcurrentHashMap<String, BigQueryReadClient> bqReadClients;
506506
private transient boolean isGlobalClientUserProvided;
507+
private transient volatile boolean closed = false;
507508

508509
/**
509510
* Lazily creates or retrieves the shared {@link BigQueryReadClient} instance used for streaming
@@ -527,6 +528,9 @@ BigQueryReadClient getBigQueryReadClient() {
527528
* @throws BigQueryException if initializing the storage read client fails
528529
*/
529530
BigQueryReadClient getBigQueryReadClient(String location) {
531+
if (closed) {
532+
throw new IllegalStateException("BigQuery service has been closed");
533+
}
530534
String cacheKey = location != null ? location.toLowerCase() : "global";
531535
if (bqReadClients == null) {
532536
synchronized (this) {
@@ -543,6 +547,9 @@ BigQueryReadClient getBigQueryReadClient(String location) {
543547
return client;
544548
}
545549
synchronized (this) {
550+
if (closed) {
551+
throw new IllegalStateException("BigQuery service has been closed");
552+
}
546553
client = bqReadClients.get(cacheKey);
547554
if (client == null && isGlobalClientUserProvided) {
548555
client = bqReadClients.get("global");
@@ -552,6 +559,10 @@ BigQueryReadClient getBigQueryReadClient(String location) {
552559
configureReadSettings(settingsBuilder, getOptions());
553560
try {
554561
client = BigQueryReadClient.create(settingsBuilder.build());
562+
if (closed) {
563+
client.close();
564+
throw new IllegalStateException("BigQuery service has been closed");
565+
}
555566
if (bqReadClients.size() < MAX_CACHED_READ_CLIENTS) {
556567
bqReadClients.put(cacheKey, client);
557568
}
@@ -583,6 +594,30 @@ void setBigQueryReadClient(String location, BigQueryReadClient client) {
583594
}
584595
}
585596

597+
/**
598+
* Closes any background resources and transport channels held by this {@link BigQueryImpl},
599+
* including the underlying {@link BigQueryReadClient} instances used for Arrow query streaming.
600+
*/
601+
@Override
602+
public void close() {
603+
synchronized (this) {
604+
if (closed) {
605+
return;
606+
}
607+
closed = true;
608+
}
609+
if (bqReadClients != null) {
610+
for (BigQueryReadClient client : bqReadClients.values()) {
611+
try {
612+
client.close();
613+
} catch (Exception e) {
614+
// Ignore exceptions during teardown
615+
}
616+
}
617+
bqReadClients.clear();
618+
}
619+
}
620+
586621
/**
587622
* Configures a {@link BigQueryReadSettings.Builder} with credentials, header provider, and
588623
* universe domain mapped from the given {@link BigQueryOptions}.

java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4343,4 +4343,70 @@ void testTestIamPermissionsWhenNoPermissionsGranted() throws IOException {
43434343
.testIamPermissionsSkipExceptionTranslation(
43444344
resourceId, checkedPermissions, EMPTY_RPC_OPTIONS);
43454345
}
4346+
4347+
@Test
4348+
void testCloseClosesBigQueryReadClient() {
4349+
BigQueryReadClient mockReadClient =
4350+
mock(BigQueryReadClient.class, withSettings().withoutAnnotations());
4351+
bigquery = options.getService();
4352+
((BigQueryImpl) bigquery).setBigQueryReadClient(mockReadClient);
4353+
4354+
bigquery.close();
4355+
4356+
verify(mockReadClient, times(1)).close();
4357+
}
4358+
4359+
@Test
4360+
void testCloseIsIdempotent() {
4361+
BigQueryReadClient mockReadClient =
4362+
mock(BigQueryReadClient.class, withSettings().withoutAnnotations());
4363+
bigquery = options.getService();
4364+
((BigQueryImpl) bigquery).setBigQueryReadClient(mockReadClient);
4365+
4366+
bigquery.close();
4367+
bigquery.close();
4368+
4369+
verify(mockReadClient, times(1)).close();
4370+
}
4371+
4372+
@Test
4373+
void testCloseWithoutReadClientDoesNotThrow() {
4374+
bigquery = options.getService();
4375+
bigquery.close();
4376+
}
4377+
4378+
@Test
4379+
void testTryWithResources() {
4380+
BigQueryReadClient mockReadClient =
4381+
mock(BigQueryReadClient.class, withSettings().withoutAnnotations());
4382+
try (BigQuery bq = options.getService()) {
4383+
((BigQueryImpl) bq).setBigQueryReadClient(mockReadClient);
4384+
assertNotNull(bq);
4385+
}
4386+
verify(mockReadClient, times(1)).close();
4387+
}
4388+
4389+
@Test
4390+
void testGetBigQueryReadClientAfterCloseThrows() {
4391+
bigquery = options.getService();
4392+
bigquery.close();
4393+
assertThrows(
4394+
IllegalStateException.class, () -> ((BigQueryImpl) bigquery).getBigQueryReadClient());
4395+
}
4396+
4397+
@Test
4398+
void testCloseClosesAllRegionalBigQueryReadClients() {
4399+
BigQueryReadClient mockReadClientUs =
4400+
mock(BigQueryReadClient.class, withSettings().withoutAnnotations());
4401+
BigQueryReadClient mockReadClientEu =
4402+
mock(BigQueryReadClient.class, withSettings().withoutAnnotations());
4403+
bigquery = options.getService();
4404+
((BigQueryImpl) bigquery).setBigQueryReadClient("us", mockReadClientUs);
4405+
((BigQueryImpl) bigquery).setBigQueryReadClient("eu", mockReadClientEu);
4406+
4407+
bigquery.close();
4408+
4409+
verify(mockReadClientUs, times(1)).close();
4410+
verify(mockReadClientEu, times(1)).close();
4411+
}
43464412
}

0 commit comments

Comments
 (0)