From 5f34434fa40844de25664f35a007db3c880b43b5 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Wed, 21 Oct 2026 11:30:35 -0400 Subject: [PATCH 1/8] one logger per class --- java-bigquery-jdbc/pom.xml | 7 +- .../jdbc/BigQueryCallableStatement.java | 3 +- .../bigquery/jdbc/BigQueryConnection.java | 3 +- .../jdbc/BigQueryDatabaseMetaData.java | 3 +- .../jdbc/BigQueryPooledConnection.java | 6 +- .../bigquery/jdbc/BigQueryStatement.java | 3 +- .../cloud/bigquery/jdbc/DataSource.java | 3 +- .../jdbc/PooledConnectionListener.java | 3 +- .../jdbc/BigQueryJdbcLoggerLeakTest.java | 99 +++++++++++++++++++ .../bigquery/jdbc/it/ITBigQueryJDBCTest.java | 2 + 10 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcLoggerLeakTest.java diff --git a/java-bigquery-jdbc/pom.xml b/java-bigquery-jdbc/pom.xml index 2f344bd29c0a..c791277e678f 100644 --- a/java-bigquery-jdbc/pom.xml +++ b/java-bigquery-jdbc/pom.xml @@ -506,7 +506,12 @@ org.apache.maven.plugins maven-surefire-plugin - --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED + + --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED --add-opens=java.logging/java.util.logging=ALL-UNNAMED diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryCallableStatement.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryCallableStatement.java index e9ad6fa1292d..e1ad8925eb0a 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryCallableStatement.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryCallableStatement.java @@ -43,7 +43,8 @@ import java.util.Map; class BigQueryCallableStatement extends BigQueryPreparedStatement implements CallableStatement { - private final BigQueryJdbcCustomLogger LOG = new BigQueryJdbcCustomLogger(this.toString()); + private static final BigQueryJdbcCustomLogger LOG = + new BigQueryJdbcCustomLogger(BigQueryCallableStatement.class.getName()); private boolean lastReadWasNull = false; private Object getParameterAndTrackNull(int parameterIndex) throws SQLException { diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java index 1e1c84c8e88c..6189f7ea75be 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java @@ -92,7 +92,8 @@ */ public class BigQueryConnection extends BigQueryNoOpsConnection { - private final BigQueryJdbcCustomLogger LOG = new BigQueryJdbcCustomLogger(this.toString()); + private static final BigQueryJdbcCustomLogger LOG = + new BigQueryJdbcCustomLogger(BigQueryConnection.class.getName()); String connectionClassName = this.toString(); private final String connectionId; private static final String DEFAULT_JDBC_TOKEN_VALUE = "Google-BigQuery-JDBC-Driver"; diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java index a24522482d6e..01ca547234ae 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java @@ -91,7 +91,8 @@ * @see BigQueryStatement */ class BigQueryDatabaseMetaData implements DatabaseMetaData { - final BigQueryJdbcCustomLogger LOG = new BigQueryJdbcCustomLogger(this.toString()); + static final BigQueryJdbcCustomLogger LOG = + new BigQueryJdbcCustomLogger(BigQueryDatabaseMetaData.class.getName()); private static final String DATABASE_PRODUCT_NAME = "Google BigQuery"; private static final String DATABASE_PRODUCT_VERSION = "2.0"; private static final String DRIVER_NAME = "GoogleJDBCDriverForGoogleBigQuery"; diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPooledConnection.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPooledConnection.java index ad62ee76bcdb..6b90421d5d3d 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPooledConnection.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPooledConnection.java @@ -31,7 +31,8 @@ import javax.sql.StatementEventListener; class BigQueryPooledConnection implements PooledConnection { - private final BigQueryJdbcCustomLogger LOG = new BigQueryJdbcCustomLogger(this.toString()); + private static final BigQueryJdbcCustomLogger LOG = + new BigQueryJdbcCustomLogger(BigQueryPooledConnection.class.getName()); private String id; // Mainly for internal use private Connection bqConnection; @@ -165,7 +166,8 @@ public void removeStatementEventListener(StatementEventListener arg0) { // This class notifies the listeners or calls the listner notification methods // provided by the pooled connection. static class BigQueryPooledConnectionWrapper implements Connection { - private final BigQueryJdbcCustomLogger LOG = new BigQueryJdbcCustomLogger(this.toString()); + private static final BigQueryJdbcCustomLogger LOG = + new BigQueryJdbcCustomLogger(BigQueryPooledConnectionWrapper.class.getName()); private Connection bqConnectionDelegate; private BigQueryPooledConnection pooledConnection; private boolean closed = false; diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java index af50bf8b8df3..ab2e2186e07f 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java @@ -102,7 +102,8 @@ */ public class BigQueryStatement extends BigQueryNoOpsStatement { - private final BigQueryJdbcCustomLogger LOG = new BigQueryJdbcCustomLogger(this.toString()); + private static final BigQueryJdbcCustomLogger LOG = + new BigQueryJdbcCustomLogger(BigQueryStatement.class.getName()); public static final int DEFAULT_BUFFER_SIZE = BigQuerySettings.DEFAULT_NUM_BUFFERED_ROWS * 2; private static final String DEFAULT_DATASET_NAME = "_google_jdbc"; private static final String DEFAULT_TABLE_NAME = "temp_table_"; diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java index d0485584e15d..66034dc1a35d 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java @@ -43,7 +43,8 @@ * registered with a naming service based on the Java™ Naming and Directory (JNDI) API. */ public class DataSource implements javax.sql.DataSource { - private final BigQueryJdbcCustomLogger LOG = new BigQueryJdbcCustomLogger(this.toString()); + private static final BigQueryJdbcCustomLogger LOG = + new BigQueryJdbcCustomLogger(DataSource.class.getName()); private String URL; static final ImmutableSet VALID_JOB_CREATION_MODES = ImmutableSet.of(1, 2); diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/PooledConnectionListener.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/PooledConnectionListener.java index 1534a6a0b798..86bc9626a08b 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/PooledConnectionListener.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/PooledConnectionListener.java @@ -23,7 +23,8 @@ import javax.sql.PooledConnection; public class PooledConnectionListener implements ConnectionEventListener { - private final BigQueryJdbcCustomLogger LOG = new BigQueryJdbcCustomLogger(this.toString()); + private static final BigQueryJdbcCustomLogger LOG = + new BigQueryJdbcCustomLogger(PooledConnectionListener.class.getName()); private String id; // Mainly for internal use private LinkedBlockingDeque connectionPool; private Long connectionPoolSize = 0L; diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcLoggerLeakTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcLoggerLeakTest.java new file mode 100644 index 000000000000..5353d791e24a --- /dev/null +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcLoggerLeakTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.bigquery.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; + +/** + * Regression tests for logger registration leaks. + * + *

Every {@code java.util.logging.Logger} keeps a list of its child loggers so that level changes + * on a parent propagate downwards. {@code Logger.setParent} appends to that list under a single + * JVM-wide lock, and the entry is never removed when the child becomes garbage unless something + * else happens to call {@code LogManager.addLogger}. A JDBC object that creates its own logger + * therefore leaves a permanent entry on the shared driver root logger and makes every later + * creation scan a longer list. These tests assert that constructing driver objects does not grow + * that list. + */ +public class BigQueryJdbcLoggerLeakTest { + + private static final int OBJECT_COUNT = 1_000; + + @Test + public void testCreatingStatementsDoesNotRegisterChildLoggers() throws Exception { + BigQueryConnection connection = mock(BigQueryConnection.class); + doReturn("leak-test-connection").when(connection).getConnectionId(); + doReturn(BigQueryConnection.SessionState.empty()).when(connection).getSessionStateSnapshot(); + + Logger rootLogger = BigQueryJdbcRootLogger.getRootLogger(); + // Warm up so that any one-time, class-level logger is registered before we measure. + new BigQueryStatement(connection); + int childrenBefore = childLoggerCount(rootLogger); + + for (int i = 0; i < OBJECT_COUNT; i++) { + new BigQueryStatement(connection); + } + + assertEquals( + childrenBefore, + childLoggerCount(rootLogger), + "Creating " + + OBJECT_COUNT + + " statements must not register additional child loggers on the driver root logger"); + } + + @Test + public void testCreatingDatabaseMetaDataDoesNotRegisterChildLoggers() + throws ReflectiveOperationException { + BigQueryConnection connection = mock(BigQueryConnection.class); + doReturn("leak-test-connection").when(connection).getConnectionId(); + + Logger rootLogger = BigQueryJdbcRootLogger.getRootLogger(); + new BigQueryDatabaseMetaData(connection); + int childrenBefore = childLoggerCount(rootLogger); + + for (int i = 0; i < OBJECT_COUNT; i++) { + new BigQueryDatabaseMetaData(connection); + } + + assertEquals( + childrenBefore, + childLoggerCount(rootLogger), + "Creating " + + OBJECT_COUNT + + " DatabaseMetaData objects must not register additional child loggers on the driver" + + " root logger"); + } + + /** + * Reads the JDK-internal {@code Logger.kids} list. This field is private to {@code java.logging}, + * so the surefire {@code argLine} opens {@code java.util.logging} to the test classpath. + */ + private static int childLoggerCount(Logger logger) throws ReflectiveOperationException { + Field kids = Logger.class.getDeclaredField("kids"); + kids.setAccessible(true); + List children = (List) kids.get(logger); + return children == null ? 0 : children.size(); + } +} diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java index 366e95b70d46..75dd454d729c 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java @@ -230,6 +230,8 @@ public void testReadAPIPathLarge() throws SQLException { withReadApi.setProperty("HighThroughputActivationRatio", "2"); withReadApi.setProperty("HighThroughputMinTableSize", "1000"); withReadApi.setProperty("MaxResults", "300"); + withReadApi.setProperty("LogLevel", "6"); + withReadApi.setProperty("LogPath", "./logs"); Connection connection = DriverManager.getConnection(String.format(connectionUrl, DEFAULT_CATALOG), withReadApi); From b7c9cfa2488be753843ce9b57b5cc51f1a5be5ca Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Wed, 21 Oct 2026 11:54:24 -0400 Subject: [PATCH 2/8] remove duplicate setParent --- java-bigquery-jdbc/pom.xml | 7 +------ .../cloud/bigquery/jdbc/BigQueryJdbcCustomLogger.java | 1 - .../cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java | 7 +++++++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/java-bigquery-jdbc/pom.xml b/java-bigquery-jdbc/pom.xml index c791277e678f..2f344bd29c0a 100644 --- a/java-bigquery-jdbc/pom.xml +++ b/java-bigquery-jdbc/pom.xml @@ -506,12 +506,7 @@ org.apache.maven.plugins maven-surefire-plugin - - --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED --add-opens=java.logging/java.util.logging=ALL-UNNAMED + --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLogger.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLogger.java index c4bc39f7ee2a..0acea77e5bf6 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLogger.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLogger.java @@ -30,7 +30,6 @@ protected BigQueryJdbcCustomLogger(String name, String resourceBundleName) { public BigQueryJdbcCustomLogger(String name) { this(name, null); - this.setParent(BigQueryJdbcRootLogger.getRootLogger()); } private void logWithCaller(Level level, Supplier msgSupplier) { diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java index 38a5df969406..2f567ccfd908 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java @@ -579,4 +579,11 @@ public void testNestedArrayInJsonStructConnectionIdPropagation() throws Exceptio "Log from Nested JSON Array", () -> nestedArray.LOG.fine("Log from Nested JSON Array")); } + + @Test + public void testLoggerIsParentedToDriverRootLogger() { + assertSame( + BigQueryJdbcRootLogger.getRootLogger(), + new BigQueryJdbcCustomLogger("ParentCheck").getParent()); + } } From 702cc5cec5925fc38da964b4ce5c1b2b06ca90d0 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Wed, 21 Oct 2026 12:19:13 -0400 Subject: [PATCH 3/8] add tests --- java-bigquery-jdbc/pom.xml | 7 +- .../jdbc/BigQueryJdbcCustomLoggerTest.java | 63 ++++++++++++ .../jdbc/BigQueryJdbcLoggerLeakTest.java | 99 ------------------- 3 files changed, 69 insertions(+), 100 deletions(-) delete mode 100644 java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcLoggerLeakTest.java diff --git a/java-bigquery-jdbc/pom.xml b/java-bigquery-jdbc/pom.xml index 2f344bd29c0a..844a9876605f 100644 --- a/java-bigquery-jdbc/pom.xml +++ b/java-bigquery-jdbc/pom.xml @@ -506,7 +506,12 @@ org.apache.maven.plugins maven-surefire-plugin - --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED + + --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED --add-opens=java.logging/java.util.logging=ALL-UNNAMED diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java index 2f567ccfd908..ab6fa432b770 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java @@ -20,6 +20,8 @@ import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import com.google.cloud.bigquery.Field; import com.google.cloud.bigquery.FieldList; @@ -37,6 +39,7 @@ import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; +import java.util.logging.Logger; import org.apache.arrow.vector.util.JsonStringArrayList; import org.apache.arrow.vector.util.JsonStringHashMap; import org.junit.jupiter.api.AfterEach; @@ -49,6 +52,8 @@ public class BigQueryJdbcCustomLoggerTest extends BigQueryJdbcLoggingBaseTest { private BigQueryJdbcCustomLogger logger; private TestHandler testHandler; + private static final int OBJECT_COUNT = 1_000; + private static class TestHandler extends Handler { private final List records = new ArrayList<>(); @@ -586,4 +591,62 @@ public void testLoggerIsParentedToDriverRootLogger() { BigQueryJdbcRootLogger.getRootLogger(), new BigQueryJdbcCustomLogger("ParentCheck").getParent()); } + + @Test + public void testCreatingStatementsDoesNotRegisterChildLoggers() throws Exception { + BigQueryConnection connection = mock(BigQueryConnection.class); + doReturn("leak-test-connection").when(connection).getConnectionId(); + doReturn(BigQueryConnection.SessionState.empty()).when(connection).getSessionStateSnapshot(); + + Logger rootLogger = BigQueryJdbcRootLogger.getRootLogger(); + // Warm up so that any one-time, class-level logger is registered before we measure. + new BigQueryStatement(connection); + int childrenBefore = childLoggerCount(rootLogger); + + for (int i = 0; i < OBJECT_COUNT; i++) { + new BigQueryStatement(connection); + } + + assertEquals( + childrenBefore, + childLoggerCount(rootLogger), + "Creating " + + OBJECT_COUNT + + " statements must not register additional child loggers on the driver root logger"); + } + + @Test + public void testCreatingDatabaseMetaDataDoesNotRegisterChildLoggers() + throws ReflectiveOperationException { + BigQueryConnection connection = mock(BigQueryConnection.class); + doReturn("leak-test-connection").when(connection).getConnectionId(); + + Logger rootLogger = BigQueryJdbcRootLogger.getRootLogger(); + new BigQueryDatabaseMetaData(connection); + int childrenBefore = childLoggerCount(rootLogger); + + for (int i = 0; i < OBJECT_COUNT; i++) { + new BigQueryDatabaseMetaData(connection); + } + + assertEquals( + childrenBefore, + childLoggerCount(rootLogger), + "Creating " + + OBJECT_COUNT + + " DatabaseMetaData objects must not register additional child loggers on the driver" + + " root logger"); + } + + /** + * Reads the JDK-internal {@code Logger.kids} list. This field is private to {@code java.logging}, + * so the surefire {@code argLine} opens {@code java.util.logging} to the test classpath. + */ + private static int childLoggerCount(Logger logger) throws ReflectiveOperationException { + // Qualified because this file already imports com.google.cloud.bigquery.Field. + java.lang.reflect.Field kids = Logger.class.getDeclaredField("kids"); + kids.setAccessible(true); + List children = (List) kids.get(logger); + return children == null ? 0 : children.size(); + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcLoggerLeakTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcLoggerLeakTest.java deleted file mode 100644 index 5353d791e24a..000000000000 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcLoggerLeakTest.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.bigquery.jdbc; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -import java.lang.reflect.Field; -import java.util.List; -import java.util.logging.Logger; -import org.junit.jupiter.api.Test; - -/** - * Regression tests for logger registration leaks. - * - *

Every {@code java.util.logging.Logger} keeps a list of its child loggers so that level changes - * on a parent propagate downwards. {@code Logger.setParent} appends to that list under a single - * JVM-wide lock, and the entry is never removed when the child becomes garbage unless something - * else happens to call {@code LogManager.addLogger}. A JDBC object that creates its own logger - * therefore leaves a permanent entry on the shared driver root logger and makes every later - * creation scan a longer list. These tests assert that constructing driver objects does not grow - * that list. - */ -public class BigQueryJdbcLoggerLeakTest { - - private static final int OBJECT_COUNT = 1_000; - - @Test - public void testCreatingStatementsDoesNotRegisterChildLoggers() throws Exception { - BigQueryConnection connection = mock(BigQueryConnection.class); - doReturn("leak-test-connection").when(connection).getConnectionId(); - doReturn(BigQueryConnection.SessionState.empty()).when(connection).getSessionStateSnapshot(); - - Logger rootLogger = BigQueryJdbcRootLogger.getRootLogger(); - // Warm up so that any one-time, class-level logger is registered before we measure. - new BigQueryStatement(connection); - int childrenBefore = childLoggerCount(rootLogger); - - for (int i = 0; i < OBJECT_COUNT; i++) { - new BigQueryStatement(connection); - } - - assertEquals( - childrenBefore, - childLoggerCount(rootLogger), - "Creating " - + OBJECT_COUNT - + " statements must not register additional child loggers on the driver root logger"); - } - - @Test - public void testCreatingDatabaseMetaDataDoesNotRegisterChildLoggers() - throws ReflectiveOperationException { - BigQueryConnection connection = mock(BigQueryConnection.class); - doReturn("leak-test-connection").when(connection).getConnectionId(); - - Logger rootLogger = BigQueryJdbcRootLogger.getRootLogger(); - new BigQueryDatabaseMetaData(connection); - int childrenBefore = childLoggerCount(rootLogger); - - for (int i = 0; i < OBJECT_COUNT; i++) { - new BigQueryDatabaseMetaData(connection); - } - - assertEquals( - childrenBefore, - childLoggerCount(rootLogger), - "Creating " - + OBJECT_COUNT - + " DatabaseMetaData objects must not register additional child loggers on the driver" - + " root logger"); - } - - /** - * Reads the JDK-internal {@code Logger.kids} list. This field is private to {@code java.logging}, - * so the surefire {@code argLine} opens {@code java.util.logging} to the test classpath. - */ - private static int childLoggerCount(Logger logger) throws ReflectiveOperationException { - Field kids = Logger.class.getDeclaredField("kids"); - kids.setAccessible(true); - List children = (List) kids.get(logger); - return children == null ? 0 : children.size(); - } -} From 73e5d7382cae731933b64ba55be06b7234b27f18 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Wed, 21 Oct 2026 12:26:36 -0400 Subject: [PATCH 4/8] cleanup --- .../com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java index 75dd454d729c..366e95b70d46 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java @@ -230,8 +230,6 @@ public void testReadAPIPathLarge() throws SQLException { withReadApi.setProperty("HighThroughputActivationRatio", "2"); withReadApi.setProperty("HighThroughputMinTableSize", "1000"); withReadApi.setProperty("MaxResults", "300"); - withReadApi.setProperty("LogLevel", "6"); - withReadApi.setProperty("LogPath", "./logs"); Connection connection = DriverManager.getConnection(String.format(connectionUrl, DEFAULT_CATALOG), withReadApi); From 429ae8d921ccdc2107a9b99174e4a36a4e66df34 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Wed, 21 Oct 2026 13:01:03 -0400 Subject: [PATCH 5/8] fix(bigquery-jdbc): stop per-ResultSet logger registration leak --- .../jdbc/BigQueryJdbcResultSetLogger.java | 144 ++++++++----- .../jdbc/BigQueryJdbcCustomLoggerTest.java | 204 +++++++++++------- 2 files changed, 215 insertions(+), 133 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java index 9d5531719f5d..8fd16152168c 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java @@ -22,36 +22,73 @@ import java.util.logging.LogRecord; /** - * Specialized high-performance logger for ResultSet and its related classes. Avoids expensive stack - * trace walking (caller inference) on hot-path logging calls. + * High-performance logging facade for ResultSet and its related classes. + * + *

This is deliberately not a {@link java.util.logging.Logger}. Every {@code Logger} + * instance registers itself as a child of its parent under a JVM-wide lock and that registration is + * never released, so a logger-per-ResultSet leaks. Instead, one real {@link + * BigQueryJdbcCustomLogger} is created per class and shared; this facade holds a reference to it + * plus the connection ID of the ResultSet it serves. + * + *

Hot-path methods ({@code finestTrace} etc.) set the source class and method explicitly, so no + * stack-trace walking (caller inference) happens for them. The connection ID is stamped into {@code + * LogRecord.getParameters()[0]} on every record so {@code PerConnectionFileHandler} can route the + * record even when the thread-local MDC (mapped diagnostic context — the per-thread connection ID + * set by the JDBC proxy) is empty, which is the normal case for un-proxied ResultSet calls. */ -public class BigQueryJdbcResultSetLogger extends BigQueryJdbcCustomLogger { - private static final ConcurrentHashMap cache = +public final class BigQueryJdbcResultSetLogger { + + /** One registered logger per class. Bounded by the number of result-set classes. */ + private static final ConcurrentHashMap DELEGATES = new ConcurrentHashMap<>(); + /** Facades with no connection ID are immutable and shared, matching the old caching behaviour. */ + private static final ConcurrentHashMap GLOBAL_FACADES = + new ConcurrentHashMap<>(); + + private final BigQueryJdbcCustomLogger delegate; private final String targetClassName; private final String connectionId; + private BigQueryJdbcResultSetLogger arrowStructLogger; + private BigQueryJdbcResultSetLogger jsonStructLogger; + private BigQueryJdbcResultSetLogger arrowArrayLogger; + private BigQueryJdbcResultSetLogger jsonArrayLogger; + + private BigQueryJdbcResultSetLogger( + BigQueryJdbcCustomLogger delegate, String targetClassName, String connectionId) { + this.delegate = delegate; + this.targetClassName = targetClassName; + this.connectionId = connectionId; + } + public static BigQueryJdbcResultSetLogger getLogger(Class clazz) { return getLogger(clazz, null); } public static BigQueryJdbcResultSetLogger getLogger(Class clazz, String connectionId) { + String className = clazz.getName(); + BigQueryJdbcCustomLogger delegate = + DELEGATES.computeIfAbsent(className, BigQueryJdbcCustomLogger::new); if (connectionId == null) { - return cache.computeIfAbsent( - clazz.getName(), k -> new BigQueryJdbcResultSetLogger(clazz, null)); + return GLOBAL_FACADES.computeIfAbsent( + className, k -> new BigQueryJdbcResultSetLogger(delegate, k, null)); } - return new BigQueryJdbcResultSetLogger(clazz, connectionId); + return new BigQueryJdbcResultSetLogger(delegate, className, connectionId); } public String getConnectionId() { return connectionId; } - private BigQueryJdbcResultSetLogger arrowStructLogger; - private BigQueryJdbcResultSetLogger jsonStructLogger; - private BigQueryJdbcResultSetLogger arrowArrayLogger; - private BigQueryJdbcResultSetLogger jsonArrayLogger; + /** The shared, registered logger this facade forwards to. Exposed for tests only. */ + BigQueryJdbcCustomLogger delegate() { + return delegate; + } + + public boolean isLoggable(Level level) { + return delegate.isLoggable(level); + } public BigQueryJdbcResultSetLogger getArrowStructLogger() { if (arrowStructLogger == null) { @@ -81,20 +118,13 @@ public BigQueryJdbcResultSetLogger getJsonArrayLogger() { return jsonArrayLogger; } - public BigQueryJdbcResultSetLogger(Class clazz) { - this(clazz, null); - } - - public BigQueryJdbcResultSetLogger(Class clazz, String connectionId) { - super(clazz.getName()); - this.targetClassName = clazz.getName(); - this.connectionId = connectionId; - } - - @Override + /** + * Stamps the connection ID into {@code params[0]} and forwards to the shared logger. Does not + * touch the record's source class/method: hot-path callers set them explicitly, and cold-path + * callers ({@code severe}, {@code warning}) rely on {@link + * BigQueryJdbcCustomLogger.BigQueryJdbcLogRecord} to infer them lazily. + */ public void log(LogRecord record) { - record.setSourceClassName(targetClassName); - record.setThreadID((int) Thread.currentThread().getId()); if (connectionId != null) { Object[] existingParams = record.getParameters(); if (existingParams == null || existingParams.length == 0) { @@ -106,93 +136,103 @@ public void log(LogRecord record) { record.setParameters(newParams); } } - super.log(record); + delegate.log(record); } private void logTrace(Level level, String methodName, String msg) { - if (isLoggable(level)) { - LogRecord record = new LogRecord(level, msg); - record.setSourceMethodName(methodName); - this.log(record); + if (!delegate.isLoggable(level)) { + return; } + LogRecord record = new LogRecord(level, msg); + record.setSourceClassName(targetClassName); + record.setSourceMethodName(methodName); + log(record); } private void logTrace(Level level, String methodName, Supplier msgSupplier) { - if (isLoggable(level)) { - LogRecord record = new LogRecord(level, msgSupplier.get()); - record.setSourceMethodName(methodName); - this.log(record); + if (!delegate.isLoggable(level)) { + return; } + logTrace(level, methodName, msgSupplier.get()); } - /** - * Log a message at Level.FINEST with predefined class name, method name and messageto avoid stack - * trace parsing on the hot-path. - */ + /** Cold path: builds a record whose caller is inferred lazily by the formatter. */ + private void logWithCaller(Level level, Throwable thrown, String msg) { + if (!delegate.isLoggable(level)) { + return; + } + LogRecord record = new BigQueryJdbcCustomLogger.BigQueryJdbcLogRecord(level, msg); + record.setThrown(thrown); + log(record); + } + + /** Log at FINEST with the class name, method name and "++enter++" preset (no stack walk). */ public void finestTrace(String methodName) { logTrace(Level.FINEST, methodName, "++enter++"); } - /** - * Log a message at Level.FINEST with predefined class name and method name to avoid stack trace - * parsing on the hot-path. - */ + /** Log at FINEST with the class name and method name preset (no stack walk). */ public void finestTrace(String methodName, String msg) { logTrace(Level.FINEST, methodName, msg); } - /** Log a lazy message at Level.FINEST with predefined class name and method name. */ + /** Log a lazy message at FINEST with the class name and method name preset. */ public void finestTrace(String methodName, Supplier msgSupplier) { logTrace(Level.FINEST, methodName, msgSupplier); } - /** Log a message at Level.FINER with predefined class name and method name. */ + /** Log at FINER with the class name and method name preset. */ public void finerTrace(String methodName, String msg) { logTrace(Level.FINER, methodName, msg); } - /** Log a lazy message at Level.FINER with predefined class name and method name. */ + /** Log a lazy message at FINER with the class name and method name preset. */ public void finerTrace(String methodName, Supplier msgSupplier) { logTrace(Level.FINER, methodName, msgSupplier); } - /** Log a message at Level.FINE with predefined class name and method name. */ + /** Log at FINE with the class name and method name preset. */ public void fineTrace(String methodName, String msg) { logTrace(Level.FINE, methodName, msg); } - /** Log a lazy message at Level.FINE with predefined class name and method name. */ + /** Log a lazy message at FINE with the class name and method name preset. */ public void fineTrace(String methodName, Supplier msgSupplier) { logTrace(Level.FINE, methodName, msgSupplier); } - @Override public void finest(String msg) { logTrace(Level.FINEST, "unknown", msg); } - @Override public void finest(Supplier msgSupplier) { logTrace(Level.FINEST, "unknown", msgSupplier); } - @Override public void finer(String msg) { logTrace(Level.FINER, "unknown", msg); } - @Override public void finer(Supplier msgSupplier) { logTrace(Level.FINER, "unknown", msgSupplier); } - @Override public void fine(String msg) { logTrace(Level.FINE, "unknown", msg); } - @Override public void fine(Supplier msgSupplier) { logTrace(Level.FINE, "unknown", msgSupplier); } + + void warning(String format, Object... args) { + if (!delegate.isLoggable(Level.WARNING)) { + return; + } + logWithCaller(Level.WARNING, null, String.format(format, args)); + } + + public void severe(String msg, Throwable thrown) { + logWithCaller(Level.SEVERE, thrown, msg); + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java index ab6fa432b770..d07052c07235 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java @@ -197,53 +197,60 @@ public void testLogWithException() { @Test public void testResultSetLoggerTrace() { BigQueryJdbcResultSetLogger rsLogger = - new BigQueryJdbcResultSetLogger(BigQueryBaseResultSet.class); + BigQueryJdbcResultSetLogger.getLogger(BigQueryBaseResultSet.class); TestHandler rsHandler = new TestHandler(); - rsLogger.addHandler(rsHandler); - rsLogger.setLevel(Level.ALL); + rsLogger.delegate().addHandler(rsHandler); + rsLogger.delegate().setLevel(Level.ALL); + try { - rsLogger.finestTrace("customMethod", "Hello finest trace message"); + rsLogger.finestTrace("customMethod", "Hello finest trace message"); - List records = rsHandler.getRecords(); - assertEquals(1, records.size()); - LogRecord record = records.get(0); + List records = rsHandler.getRecords(); + assertEquals(1, records.size()); + LogRecord record = records.get(0); - assertEquals(Level.FINEST, record.getLevel()); - assertEquals("customMethod", record.getSourceMethodName()); - assertEquals(BigQueryBaseResultSet.class.getName(), record.getSourceClassName()); - assertEquals("Hello finest trace message", record.getMessage()); + assertEquals(Level.FINEST, record.getLevel()); + assertEquals("customMethod", record.getSourceMethodName()); + assertEquals(BigQueryBaseResultSet.class.getName(), record.getSourceClassName()); + assertEquals("Hello finest trace message", record.getMessage()); + } finally { + rsLogger.delegate().removeHandler(rsHandler); + } } @Test public void testResultSetLoggerStandardMethods() { BigQueryJdbcResultSetLogger rsLogger = - new BigQueryJdbcResultSetLogger(BigQueryBaseResultSet.class); + BigQueryJdbcResultSetLogger.getLogger(BigQueryBaseResultSet.class); TestHandler rsHandler = new TestHandler(); - rsLogger.addHandler(rsHandler); - rsLogger.setLevel(Level.ALL); - - rsLogger.finest("Finest msg"); - rsLogger.finer("Finer msg"); - rsLogger.fine("Fine msg"); - - List records = rsHandler.getRecords(); - assertEquals(3, records.size()); - - LogRecord r1 = records.get(0); - assertEquals(Level.FINEST, r1.getLevel()); - assertEquals("unknown", r1.getSourceMethodName()); - assertEquals(BigQueryBaseResultSet.class.getName(), r1.getSourceClassName()); - assertEquals("Finest msg", r1.getMessage()); - - LogRecord r2 = records.get(1); - assertEquals(Level.FINER, r2.getLevel()); - assertEquals("unknown", r2.getSourceMethodName()); - assertEquals("Finer msg", r2.getMessage()); - - LogRecord r3 = records.get(2); - assertEquals(Level.FINE, r3.getLevel()); - assertEquals("unknown", r3.getSourceMethodName()); - assertEquals("Fine msg", r3.getMessage()); + rsLogger.delegate().addHandler(rsHandler); + rsLogger.delegate().setLevel(Level.ALL); + try { + rsLogger.finest("Finest msg"); + rsLogger.finer("Finer msg"); + rsLogger.fine("Fine msg"); + + List records = rsHandler.getRecords(); + assertEquals(3, records.size()); + + LogRecord r1 = records.get(0); + assertEquals(Level.FINEST, r1.getLevel()); + assertEquals("unknown", r1.getSourceMethodName()); + assertEquals(BigQueryBaseResultSet.class.getName(), r1.getSourceClassName()); + assertEquals("Finest msg", r1.getMessage()); + + LogRecord r2 = records.get(1); + assertEquals(Level.FINER, r2.getLevel()); + assertEquals("unknown", r2.getSourceMethodName()); + assertEquals("Finer msg", r2.getMessage()); + + LogRecord r3 = records.get(2); + assertEquals(Level.FINE, r3.getLevel()); + assertEquals("unknown", r3.getSourceMethodName()); + assertEquals("Fine msg", r3.getMessage()); + } finally { + rsLogger.delegate().removeHandler(rsHandler); + } } @Test @@ -263,21 +270,24 @@ public void testCustomLoggerSupplierMethods() { @Test public void testResultSetLoggerSupplierMethods() { BigQueryJdbcResultSetLogger rsLogger = - new BigQueryJdbcResultSetLogger(BigQueryBaseResultSet.class); + BigQueryJdbcResultSetLogger.getLogger(BigQueryBaseResultSet.class); TestHandler rsHandler = new TestHandler(); - rsLogger.addHandler(rsHandler); - rsLogger.setLevel(Level.ALL); - - rsLogger.finest(() -> "Lazy finest RS message"); + rsLogger.delegate().addHandler(rsHandler); + rsLogger.delegate().setLevel(Level.ALL); + try { + rsLogger.finest(() -> "Lazy finest RS message"); - List records = rsHandler.getRecords(); - assertEquals(1, records.size()); - LogRecord record = records.get(0); + List records = rsHandler.getRecords(); + assertEquals(1, records.size()); + LogRecord record = records.get(0); - assertEquals(Level.FINEST, record.getLevel()); - assertEquals("unknown", record.getSourceMethodName()); - assertEquals(BigQueryBaseResultSet.class.getName(), record.getSourceClassName()); - assertEquals("Lazy finest RS message", record.getMessage()); + assertEquals(Level.FINEST, record.getLevel()); + assertEquals("unknown", record.getSourceMethodName()); + assertEquals(BigQueryBaseResultSet.class.getName(), record.getSourceClassName()); + assertEquals("Lazy finest RS message", record.getMessage()); + } finally { + rsLogger.delegate().removeHandler(rsHandler); + } } @Test @@ -298,60 +308,70 @@ public void testSupplierNotEvaluatedWhenDisabled() { @Test public void testResultSetLoggerSupplierTraceMethods() { BigQueryJdbcResultSetLogger rsLogger = - new BigQueryJdbcResultSetLogger(BigQueryBaseResultSet.class); + BigQueryJdbcResultSetLogger.getLogger(BigQueryBaseResultSet.class); TestHandler rsHandler = new TestHandler(); - rsLogger.addHandler(rsHandler); - rsLogger.setLevel(Level.ALL); - - rsLogger.finestTrace("customTraceMethod", () -> "Lazy finest RS trace message"); + rsLogger.delegate().addHandler(rsHandler); + rsLogger.delegate().setLevel(Level.ALL); + try { + rsLogger.finestTrace("customTraceMethod", () -> "Lazy finest RS trace message"); - List records = rsHandler.getRecords(); - assertEquals(1, records.size()); - LogRecord record = records.get(0); + List records = rsHandler.getRecords(); + assertEquals(1, records.size()); + LogRecord record = records.get(0); - assertEquals(Level.FINEST, record.getLevel()); - assertEquals("customTraceMethod", record.getSourceMethodName()); - assertEquals(BigQueryBaseResultSet.class.getName(), record.getSourceClassName()); - assertEquals("Lazy finest RS trace message", record.getMessage()); + assertEquals(Level.FINEST, record.getLevel()); + assertEquals("customTraceMethod", record.getSourceMethodName()); + assertEquals(BigQueryBaseResultSet.class.getName(), record.getSourceClassName()); + assertEquals("Lazy finest RS trace message", record.getMessage()); + } finally { + rsLogger.delegate().removeHandler(rsHandler); + } } @Test public void testResultSetLoggerCachingBehavior() { - // Global logger (connectionId is null) should be cached + // Global facades (no connection ID) are shared. BigQueryJdbcResultSetLogger globalLogger1 = BigQueryJdbcResultSetLogger.getLogger(BigQueryBaseResultSet.class); BigQueryJdbcResultSetLogger globalLogger2 = BigQueryJdbcResultSetLogger.getLogger(BigQueryBaseResultSet.class); assertSame(globalLogger1, globalLogger2); - - // Connection-specific loggers should not be cached globally + // Connection-scoped facades are distinct lightweight objects, but they all forward to the + // single registered logger for the class — that is what prevents the registration leak. BigQueryJdbcResultSetLogger connLogger1 = BigQueryJdbcResultSetLogger.getLogger(BigQueryBaseResultSet.class, "conn-123"); BigQueryJdbcResultSetLogger connLogger2 = - BigQueryJdbcResultSetLogger.getLogger(BigQueryBaseResultSet.class, "conn-123"); + BigQueryJdbcResultSetLogger.getLogger(BigQueryBaseResultSet.class, "conn-456"); assertNotSame(connLogger1, connLogger2); + assertSame(connLogger1.delegate(), connLogger2.delegate()); + assertSame(globalLogger1.delegate(), connLogger1.delegate()); } @Test public void testResultSetLoggerParameterPreservation() { BigQueryJdbcResultSetLogger rsLogger = - new BigQueryJdbcResultSetLogger(BigQueryBaseResultSet.class, "conn-123"); + BigQueryJdbcResultSetLogger.getLogger(BigQueryBaseResultSet.class, "conn-123"); TestHandler rsHandler = new TestHandler(); - rsLogger.addHandler(rsHandler); - rsLogger.setLevel(Level.ALL); + rsLogger.delegate().addHandler(rsHandler); + rsLogger.delegate().setLevel(Level.ALL); - LogRecord record = new LogRecord(Level.INFO, "Processing job {0}"); - record.setParameters(new Object[] {"job-999"}); - rsLogger.log(record); + try { - List records = rsHandler.getRecords(); - assertEquals(1, records.size()); - LogRecord loggedRecord = records.get(0); + LogRecord record = new LogRecord(Level.INFO, "Processing job {0}"); + record.setParameters(new Object[] {"job-999"}); + rsLogger.log(record); + + List records = rsHandler.getRecords(); + assertEquals(1, records.size()); + LogRecord loggedRecord = records.get(0); - Object[] params = loggedRecord.getParameters(); - assertEquals(2, params.length); - assertEquals("conn-123", params[0]); - assertEquals("job-999", params[1]); + Object[] params = loggedRecord.getParameters(); + assertEquals(2, params.length); + assertEquals("conn-123", params[0]); + assertEquals("job-999", params[1]); + } finally { + rsLogger.delegate().removeHandler(rsHandler); + } } private void assertConnectionIdPropagated( @@ -360,7 +380,7 @@ private void assertConnectionIdPropagated( String expectedMessage, Runnable action) { Level originalRootLevel = BigQueryJdbcRootLogger.getRootLogger().getLevel(); - Level originalLoggerLevel = logger.getLevel(); + Level originalLoggerLevel = logger.delegate().getLevel(); Map originalHandlerLevels = new HashMap<>(); for (Handler h : BigQueryJdbcRootLogger.getRootLogger().getHandlers()) { originalHandlerLevels.put(h, h.getLevel()); @@ -368,7 +388,7 @@ private void assertConnectionIdPropagated( } BigQueryJdbcRootLogger.getRootLogger().setLevel(Level.ALL); - logger.setLevel(Level.ALL); + logger.delegate().setLevel(Level.ALL); try { capturedLogs.clear(); @@ -378,7 +398,7 @@ private void assertConnectionIdPropagated( assertTrue(assertLogContains(expectedMessage)); } finally { BigQueryJdbcRootLogger.getRootLogger().setLevel(originalRootLevel); - logger.setLevel(originalLoggerLevel); + logger.delegate().setLevel(originalLoggerLevel); for (Entry entry : originalHandlerLevels.entrySet()) { entry.getKey().setLevel(entry.getValue()); } @@ -638,6 +658,28 @@ public void testCreatingDatabaseMetaDataDoesNotRegisterChildLoggers() + " root logger"); } + @Test + public void testConnectionScopedResultSetLoggersDoNotRegisterChildLoggers() + throws ReflectiveOperationException { + Logger rootLogger = BigQueryJdbcRootLogger.getRootLogger(); + // Warm up so the per-class delegates (and their one-time registration) exist before measuring. + BigQueryJdbcResultSetLogger.getLogger(BigQueryArrowResultSet.class, "warm-up") + .getArrowStructLogger() + .getArrowArrayLogger(); + int childrenBefore = childLoggerCount(rootLogger); + + for (int i = 0; i < OBJECT_COUNT; i++) { + BigQueryJdbcResultSetLogger.getLogger(BigQueryArrowResultSet.class, "conn-" + i) + .getArrowStructLogger() + .getArrowArrayLogger(); + } + + assertEquals( + childrenBefore, + childLoggerCount(rootLogger), + "Connection-scoped result-set loggers must not register child loggers on the driver root"); + } + /** * Reads the JDK-internal {@code Logger.kids} list. This field is private to {@code java.logging}, * so the surefire {@code argLine} opens {@code java.util.logging} to the test classpath. From 2743cc9737cf50648469ad93e78859024ff26600 Mon Sep 17 00:00:00 2001 From: Neenu Shaji Date: Wed, 21 Oct 2026 13:29:17 -0400 Subject: [PATCH 6/8] Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java index 8fd16152168c..e59c29285ae2 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java @@ -229,7 +229,8 @@ void warning(String format, Object... args) { if (!delegate.isLoggable(Level.WARNING)) { return; } - logWithCaller(Level.WARNING, null, String.format(format, args)); + String message = (args == null || args.length == 0) ? format : String.format(format, args); + logWithCaller(Level.WARNING, null, message); } public void severe(String msg, Throwable thrown) { From 47ef2e8c9223ddae3ed9f42237dae559c7d1636c Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Wed, 21 Oct 2026 13:48:40 -0400 Subject: [PATCH 7/8] nit --- .../cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java index e59c29285ae2..31d3a201d61e 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java @@ -50,10 +50,10 @@ public final class BigQueryJdbcResultSetLogger { private final String targetClassName; private final String connectionId; - private BigQueryJdbcResultSetLogger arrowStructLogger; - private BigQueryJdbcResultSetLogger jsonStructLogger; - private BigQueryJdbcResultSetLogger arrowArrayLogger; - private BigQueryJdbcResultSetLogger jsonArrayLogger; + private volatile BigQueryJdbcResultSetLogger arrowStructLogger; + private volatile BigQueryJdbcResultSetLogger jsonStructLogger; + private volatile BigQueryJdbcResultSetLogger arrowArrayLogger; + private volatile BigQueryJdbcResultSetLogger jsonArrayLogger; private BigQueryJdbcResultSetLogger( BigQueryJdbcCustomLogger delegate, String targetClassName, String connectionId) { From 31d65352c96d7e52fc27f7568bff027bc984d191 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Thu, 22 Oct 2026 09:35:50 -0400 Subject: [PATCH 8/8] address comments --- .../google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java index 31d3a201d61e..2e5753fa2c46 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcResultSetLogger.java @@ -225,7 +225,7 @@ public void fine(Supplier msgSupplier) { logTrace(Level.FINE, "unknown", msgSupplier); } - void warning(String format, Object... args) { + public void warning(String format, Object... args) { if (!delegate.isLoggable(Level.WARNING)) { return; }