Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.google.cloud.bigquery.TableDefinition;
import com.google.cloud.bigquery.TableId;
import com.google.cloud.bigquery.exception.BigQueryJdbcException;
import com.google.cloud.bigquery.jdbc.utils.BigQueryJdbcVersionUtility;
import com.google.common.collect.ImmutableMap;
import java.io.BufferedReader;
import java.io.IOException;
Expand All @@ -61,7 +62,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
Expand All @@ -73,7 +74,7 @@
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
Expand All @@ -92,7 +93,7 @@ class BigQueryDatabaseMetaData implements DatabaseMetaData {
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";
private static final String DRIVER_DEFAULT_VERSION = "0.0.0";

private static final String SCHEMA_TERM = "Dataset";
private static final String CATALOG_TERM = "Project";
private static final String PROCEDURE_TERM = "Procedure";
Expand Down Expand Up @@ -143,18 +144,11 @@ class BigQueryDatabaseMetaData implements DatabaseMetaData {
BigQueryConnection connection;
private final BigQuery bigquery;
private final int metadataFetchThreadCount;
private static final AtomicReference<String> parsedDriverVersion = new AtomicReference<>(null);
private static final AtomicReference<Integer> parsedDriverMajorVersion =
new AtomicReference<>(null);
private static final AtomicReference<Integer> parsedDriverMinorVersion =
new AtomicReference<>(null);

BigQueryDatabaseMetaData(BigQueryConnection connection) {
this.URL = connection.getConnectionUrl();
this.connection = connection;
this.bigquery = connection.getBigQuery();
this.metadataFetchThreadCount = connection.getMetadataFetchThreadCount();
loadDriverVersionProperties();
}

@Override
Expand Down Expand Up @@ -223,17 +217,17 @@ public String getDriverName() {

@Override
public String getDriverVersion() {
return parsedDriverVersion.get() != null ? parsedDriverVersion.get() : DRIVER_DEFAULT_VERSION;
return BigQueryJdbcVersionUtility.getDriverVersion();
}

@Override
public int getDriverMajorVersion() {
return parsedDriverMajorVersion.get() != null ? parsedDriverMajorVersion.get() : 0;
return BigQueryJdbcVersionUtility.getDriverMajorVersion();
}

@Override
public int getDriverMinorVersion() {
return parsedDriverMinorVersion.get() != null ? parsedDriverMinorVersion.get() : 0;
return BigQueryJdbcVersionUtility.getDriverMinorVersion();
}

@Override
Expand Down Expand Up @@ -5288,48 +5282,4 @@ String replaceSqlParameters(String sql, String... params) throws SQLException {
return String.format(sql, (Object[]) params);
}

private void loadDriverVersionProperties() {
if (parsedDriverVersion.get() != null) {
return;
}
Properties props = new Properties();
try (InputStream input =
getClass().getResourceAsStream("/com/google/cloud/bigquery/jdbc/dependencies.properties")) {
if (input == null) {
String errorMessage =
"Could not find dependencies.properties. Driver version information is unavailable.";
IllegalStateException ex = new IllegalStateException(errorMessage);
LOG.severe(errorMessage, ex);
throw ex;
}
props.load(input);
String versionString = props.getProperty("version.jdbc");
if (versionString == null || versionString.trim().isEmpty()) {
String errorMessage =
"The property version.jdbc not found or empty in dependencies.properties.";
IllegalStateException ex = new IllegalStateException(errorMessage);
LOG.severe(errorMessage, ex);
throw ex;
}
parsedDriverVersion.compareAndSet(null, versionString.trim());
String[] parts = versionString.split("\\.");
if (parts.length < 2) {
return;
}
parsedDriverMajorVersion.compareAndSet(null, Integer.parseInt(parts[0]));
String minorPart = parts[1];
String numericMinor = minorPart.replaceAll("[^0-9].*", "");
if (!numericMinor.isEmpty()) {
parsedDriverMinorVersion.compareAndSet(null, Integer.parseInt(numericMinor));
}
} catch (IOException | NumberFormatException e) {
String errorMessage =
"Error reading dependencies.properties. Driver version information is"
+ " unavailable. Error: "
+ e.getMessage();
IllegalStateException ex = new IllegalStateException(errorMessage, e);
LOG.severe(errorMessage, ex);
throw ex;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.google.cloud.bigquery.exception.BigQueryJdbcException;
import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException;
import com.google.cloud.bigquery.jdbc.utils.BigQueryJdbcVersionUtility;
import io.grpc.LoadBalancerRegistry;
import io.grpc.internal.PickFirstLoadBalancerProvider;
import java.io.IOException;
Expand Down Expand Up @@ -55,9 +56,6 @@ public class BigQueryDriver implements Driver {

private static final BigQueryJdbcCustomLogger LOG =
new BigQueryJdbcCustomLogger(BigQueryDriver.class.getName());
// TODO: update this when JDBC goes GA
private static final int JDBC_MAJOR_VERSION = 0;
private static final int JDBC_MINOR_VERSION = 1;
static BigQueryDriver registeredBigqueryJdbcDriver;

static {
Expand Down Expand Up @@ -232,13 +230,13 @@ public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
@Override
public int getMajorVersion() {
LOG.finest("++enter++");
return JDBC_MAJOR_VERSION;
return BigQueryJdbcVersionUtility.getDriverMajorVersion();
}

@Override
public int getMinorVersion() {
LOG.finest("++enter++");
return JDBC_MINOR_VERSION;
return BigQueryJdbcVersionUtility.getDriverMinorVersion();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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.utils;

import com.google.cloud.bigquery.jdbc.BigQueryJdbcCustomLogger;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;

/** Utility class to load and parse the JDBC driver version from dependencies.properties. */
public final class BigQueryJdbcVersionUtility {

private static final BigQueryJdbcCustomLogger LOG =
new BigQueryJdbcCustomLogger(BigQueryJdbcVersionUtility.class.getName());

private static final AtomicReference<String> parsedDriverVersion = new AtomicReference<>(null);
private static final AtomicReference<Integer> parsedDriverMajorVersion =
new AtomicReference<>(null);
private static final AtomicReference<Integer> parsedDriverMinorVersion =
new AtomicReference<>(null);

private BigQueryJdbcVersionUtility() {
// Utility class, static methods only.
}

/**
* Gets the full driver version string.
*
* @return the driver version string, e.g., "1.0.0-SNAPSHOT"
*/
public static String getDriverVersion() {
ensureLoaded();
return parsedDriverVersion.get();
}

/**
* Gets the driver major version.
*
* @return the major version number
*/
public static int getDriverMajorVersion() {
Comment thread
logachev marked this conversation as resolved.
ensureLoaded();
return parsedDriverMajorVersion.get() != null ? parsedDriverMajorVersion.get() : 0;
}

/**
* Gets the driver minor version.
*
* @return the minor version number
*/
public static int getDriverMinorVersion() {
Comment thread
logachev marked this conversation as resolved.
ensureLoaded();
return parsedDriverMinorVersion.get() != null ? parsedDriverMinorVersion.get() : 0;
}

private static void ensureLoaded() {
if (parsedDriverVersion.get() != null) {
return;
}
Properties props = new Properties();
try (InputStream input =
BigQueryJdbcVersionUtility.class.getResourceAsStream(
"/com/google/cloud/bigquery/jdbc/dependencies.properties")) {
if (input == null) {
String errorMessage =
"Could not find dependencies.properties. Driver version information is unavailable.";
IllegalStateException ex = new IllegalStateException(errorMessage);
LOG.severe(errorMessage, ex);
throw ex;
}
props.load(input);
String versionString = props.getProperty("version.jdbc");
if (versionString == null || versionString.trim().isEmpty()) {
String errorMessage =
"The property version.jdbc not found or empty in dependencies.properties.";
IllegalStateException ex = new IllegalStateException(errorMessage);
LOG.severe(errorMessage, ex);
throw ex;
}
parsedDriverVersion.compareAndSet(null, versionString.trim());
String[] parts = versionString.split("\\.");
if (parts.length < 2) {
return;
}
parsedDriverMajorVersion.compareAndSet(null, Integer.parseInt(parts[0]));
String minorPart = parts[1];
String numericMinor = minorPart.replaceAll("[^0-9].*", "");
if (!numericMinor.isEmpty()) {
parsedDriverMinorVersion.compareAndSet(null, Integer.parseInt(numericMinor));
}
} catch (IOException | NumberFormatException e) {
String errorMessage =
"Error reading dependencies.properties. Driver version information is"
+ " unavailable. Error: "
+ e.getMessage();
IllegalStateException ex = new IllegalStateException(errorMessage, e);
LOG.severe(errorMessage, ex);
throw ex;
}
}
Comment on lines +31 to +113
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Issues Identified:

  1. Performance Bottleneck (Thread Contention): The ensureLoaded() method is marked as synchronized. Since getDriverVersion(), getDriverMajorVersion(), and getDriverMinorVersion() all call ensureLoaded(), every single invocation of these methods will acquire and release a class-level lock. In high-throughput environments, this can lead to severe thread contention.
  2. Robustness Issue (Runtime Exceptions): If dependencies.properties is missing or corrupt, ensureLoaded() throws an IllegalStateException. Standard JDBC drivers should never throw runtime exceptions from Driver.getMajorVersion() or Driver.getMinorVersion(); they should fail gracefully and return default values (e.g., 0).
  3. Race Condition in Double-Checked Locking: If we were to simply make ensureLoaded() use double-checked locking, we would have a race condition because parsedDriverVersion is set before parsedDriverMajorVersion and parsedDriverMinorVersion. A thread could see parsedDriverVersion as non-null and return early, getting null or 0 for the major/minor versions.

Solution:

We can implement a robust, lock-free double-checked locking pattern using an explicit lock (ReentrantLock) instead of the synchronized keyword to avoid thread contention and adhere to repository rules:

  • Removing the synchronized keyword from the ensureLoaded() method signature and using an explicit ReentrantLock inside it.
  • Catching all exceptions gracefully and falling back to safe defaults (0.0.0, 0, 0) instead of throwing IllegalStateException.
  • Setting parsedDriverVersion last in the locked block. Since parsedDriverVersion is an AtomicReference (which has volatile write/read semantics), setting it last acts as a memory barrier. Any thread that reads a non-null value from parsedDriverVersion is guaranteed to see the fully initialized major and minor versions.
  private static final String DEFAULT_VERSION = "0.0.0";
  private static final AtomicReference<String> parsedDriverVersion = new AtomicReference<>(null);
  private static final AtomicReference<Integer> parsedDriverMajorVersion =
      new AtomicReference<>(null);
  private static final AtomicReference<Integer> parsedDriverMinorVersion =
      new AtomicReference<>(null);
  private static final java.util.concurrent.locks.ReentrantLock LOCK = new java.util.concurrent.locks.ReentrantLock();

  private BigQueryJdbcVersionUtility() {
    // Utility class, static methods only.
  }

  /**
   * Gets the full driver version string.
   * 
   * @return the driver version string, e.g., "1.0.0-SNAPSHOT"
   */
  public static String getDriverVersion() {
    ensureLoaded();
    return parsedDriverVersion.get();
  }

  /**
   * Gets the driver major version.
   * 
   * @return the major version number
   */
  public static int getDriverMajorVersion() {
    ensureLoaded();
    return parsedDriverMajorVersion.get() != null ? parsedDriverMajorVersion.get() : 0;
  }

  /**
   * Gets the driver minor version.
   * 
   * @return the minor version number
   */
  public static int getDriverMinorVersion() {
    ensureLoaded();
    return parsedDriverMinorVersion.get() != null ? parsedDriverMinorVersion.get() : 0;
  }

  private static void ensureLoaded() {
    if (parsedDriverVersion.get() != null) {
      return;
    }
    LOCK.lock();
    try {
      if (parsedDriverVersion.get() != null) {
        return;
      }
      String versionVal = DEFAULT_VERSION;
      int majorVal = 0;
      int minorVal = 0;
      try (InputStream input =
          BigQueryJdbcVersionUtility.class.getResourceAsStream(
              "/com/google/cloud/bigquery/jdbc/dependencies.properties")) {
        if (input == null) {
          LOG.severe("Could not find dependencies.properties. Driver version information is unavailable.");
        } else {
          Properties props = new Properties();
          props.load(input);
          String versionString = props.getProperty("version.jdbc");
          if (versionString == null || versionString.trim().isEmpty()) {
            LOG.severe("The property version.jdbc not found or empty in dependencies.properties.");
          } else {
            versionVal = versionString.trim();
            String[] parts = versionVal.split("\\.");
            if (parts.length >= 1) {
              try {
                majorVal = Integer.parseInt(parts[0]);
              } catch (NumberFormatException e) {
                LOG.severe("Error parsing major version: " + parts[0], e);
              }
            }
            if (parts.length >= 2) {
              String minorPart = parts[1];
              String numericMinor = minorPart.replaceAll("[^0-9].*", "");
              if (!numericMinor.isEmpty()) {
                try {
                  minorVal = Integer.parseInt(numericMinor);
                } catch (NumberFormatException e) {
                  LOG.severe("Error parsing minor version: " + numericMinor, e);
                }
              }
            }
          }
        }
      } catch (Exception e) {
        LOG.severe("Error reading dependencies.properties. Driver version information is unavailable.", e);
      }
      parsedDriverMajorVersion.set(majorVal);
      parsedDriverMinorVersion.set(minorVal);
      parsedDriverVersion.set(versionVal);
    } finally {
      LOCK.unlock();
    }
  }
References
  1. In performance-sensitive code, prefer using explicit locks over the 'synchronized' keyword to protect shared state while ensuring thread safety and visibility.

}
Comment thread
logachev marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import static com.google.common.truth.Truth.assertThat;

import com.google.cloud.bigquery.jdbc.utils.BigQueryJdbcVersionUtility;
import java.sql.Connection;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
Expand Down Expand Up @@ -76,12 +77,12 @@ public void testGetPropertyInfoReturnsValidProperties() {

@Test
public void testGetMajorVersionMatchesDriverMajorVersion() {
assertThat(bigQueryDriver.getMajorVersion()).isEqualTo(0);
assertThat(bigQueryDriver.getMajorVersion()).isEqualTo(BigQueryJdbcVersionUtility.getDriverMajorVersion());
}

@Test
public void testGetMinorVersionMatchesDriverMinorVersion() {
assertThat(bigQueryDriver.getMinorVersion()).isEqualTo(1);
assertThat(bigQueryDriver.getMinorVersion()).isEqualTo(BigQueryJdbcVersionUtility.getDriverMinorVersion());
}

@Test
Expand Down
Loading