fix(bigquery-jdbc): add proper version to BigQueryDriver#13294
fix(bigquery-jdbc): add proper version to BigQueryDriver#13294logachev wants to merge 3 commits into
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.
|
Can we also update the README in this PR for the maven link |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request refactors the JDBC driver version loading and parsing logic by extracting it from BigQueryDatabaseMetaData into a new utility class, BigQueryJdbcVersionUtility. Both BigQueryDatabaseMetaData and BigQueryDriver are updated to delegate version retrieval to this utility. Feedback on the new utility class points out potential thread contention from the synchronized ensureLoaded() method, risk of runtime exceptions, and a race condition during initialization. A more robust implementation using explicit locks and safe default fallbacks is suggested.
| 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 synchronized 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(); | ||
| LOG.severe(errorMessage, e); | ||
| } | ||
| } |
There was a problem hiding this comment.
Issues Identified:
- Performance Bottleneck (Thread Contention): The
ensureLoaded()method is marked assynchronized. SincegetDriverVersion(),getDriverMajorVersion(), andgetDriverMinorVersion()all callensureLoaded(), 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. - Robustness Issue (Runtime Exceptions): If
dependencies.propertiesis missing or corrupt,ensureLoaded()throws anIllegalStateException. Standard JDBC drivers should never throw runtime exceptions fromDriver.getMajorVersion()orDriver.getMinorVersion(); they should fail gracefully and return default values (e.g.,0). - Race Condition in Double-Checked Locking: If we were to simply make
ensureLoaded()use double-checked locking, we would have a race condition becauseparsedDriverVersionis set beforeparsedDriverMajorVersionandparsedDriverMinorVersion. A thread could seeparsedDriverVersionas non-null and return early, gettingnullor0for 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
synchronizedkeyword from theensureLoaded()method signature and using an explicitReentrantLockinside it. - Catching all exceptions gracefully and falling back to safe defaults (
0.0.0,0,0) instead of throwingIllegalStateException. - Setting
parsedDriverVersionlast in the locked block. SinceparsedDriverVersionis anAtomicReference(which has volatile write/read semantics), setting it last acts as a memory barrier. Any thread that reads a non-null value fromparsedDriverVersionis 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
- In performance-sensitive code, prefer using explicit locks over the 'synchronized' keyword to protect shared state while ensuring thread safety and visibility.
Update
BigQueryDrivergetMinorVersion/getMajorVersion to return proper value