fix(bigquery-jdbc): add proper version to BigQueryDriver#13294
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the JDBC driver version loading and parsing logic by moving it from BigQueryDatabaseMetaData into a new utility class, BigQueryJdbcVersionUtility, which is then used by both BigQueryDatabaseMetaData and BigQueryDriver. A critical review comment points out a concurrency race condition in the lazy initialization of the utility class, which could lead to threads reading uninitialized major or minor versions. Additionally, the comment highlights a bug where single-part version strings would prevent the major version from being set. The reviewer suggests resolving these issues by using a thread-safe static initializer block to load the properties.
| 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() { | ||
| 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; | ||
| } | ||
| 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; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Concurrency Race Condition & Robustness Issue
There is a race condition in the lazy initialization logic of ensureLoaded(). If multiple threads call ensureLoaded() concurrently, one thread (Thread A) can set parsedDriverVersion but get context-switched before setting parsedDriverMajorVersion or parsedDriverMinorVersion. If another thread (Thread B) then calls getDriverMajorVersion(), it will see parsedDriverVersion.get() != null as true, skip loading, and return 0 (since parsedDriverMajorVersion is still null).
Additionally, if the version string has only one part (e.g., "1"), parts.length < 2 causes an early return, meaning the major version is never set and defaults to 0.
Solution
Since this is a utility class loading static classpath resources, using a static initializer block is the most standard, robust, and thread-safe approach. It completely eliminates the race condition, removes the need for AtomicReferences and ensureLoaded() overhead, and makes the fields private static final.
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;
/** 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 String driverVersion;
private static final int driverMajorVersion;
private static final int driverMinorVersion;
static {
String version = "0.0.0";
int major = 0;
int minor = 0;
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;
}
Properties props = new Properties();
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;
}
version = versionString.trim();
String[] parts = version.split("\\.");
if (parts.length > 0) {
major = Integer.parseInt(parts[0]);
}
if (parts.length > 1) {
String minorPart = parts[1];
String numericMinor = minorPart.replaceAll("[^0-9].*", "");
if (!numericMinor.isEmpty()) {
minor = 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;
}
driverVersion = version;
driverMajorVersion = major;
driverMinorVersion = minor;
}
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() {
return driverVersion;
}
/**
* Gets the driver major version.
*
* @return the major version number
*/
public static int getDriverMajorVersion() {
return driverMajorVersion;
}
/**
* Gets the driver minor version.
*
* @return the minor version number
*/
public static int getDriverMinorVersion() {
return driverMinorVersion;
}
}There was a problem hiding this comment.
Agree with gemini, we should address concurrency issue here
| * | ||
| * @return the major version number | ||
| */ | ||
| public static int getDriverMajorVersion() { |
There was a problem hiding this comment.
So, this and the minorVersion function with this new impl could throw IllegalStateException for BigQueryDriver.getMajorVersion() and getMinorVersion(), right?
I think we need to confirm JDBC spec if that's okay or should it throw SQL exception or should it default to something and not throw error at all
| * | ||
| * @return the minor version number | ||
| */ | ||
| public static int getDriverMinorVersion() { |
| 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() { | ||
| 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; | ||
| } | ||
| 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; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Agree with gemini, we should address concurrency issue here
|
Can we also update the README in this PR for the maven link |
Update
BigQueryDrivergetMinorVersion/getMajorVersion to return proper value