Skip to content

Commit

Permalink
Retry SQLErrorCodesFactory retrieval if DatabaseMetaData access failed
Browse files Browse the repository at this point in the history
Includes deprecation of JdbcUtils.extractDatabaseMetaData(DataSource, String) in favor of the now generified version of extractDatabaseMetaData(DataSource, DatabaseMetaDataCallback).

Closes gh-25681
Closes gh-25686
  • Loading branch information
jhoeller committed Sep 3, 2020
1 parent c2363a6 commit 670b9fd
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 72 deletions.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,6 +17,7 @@
package org.springframework.scheduling.quartz;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;

import javax.sql.DataSource;
Expand Down Expand Up @@ -147,7 +148,8 @@ public void initialize() {

// No, if HSQL is the platform, we really don't want to use locks...
try {
String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource, "getDatabaseProductName");
String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource,
DatabaseMetaData::getDatabaseProductName);
productName = JdbcUtils.commonDatabaseName(productName);
if (productName != null && productName.toLowerCase().contains("hsql")) {
setUseDBLocks(false);
Expand Down
Expand Up @@ -73,7 +73,7 @@ private CallMetaDataProviderFactory() {
*/
public static CallMetaDataProvider createMetaDataProvider(DataSource dataSource, final CallMetaDataContext context) {
try {
return (CallMetaDataProvider) JdbcUtils.extractDatabaseMetaData(dataSource, databaseMetaData -> {
return JdbcUtils.extractDatabaseMetaData(dataSource, databaseMetaData -> {
String databaseProductName = JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName());
boolean accessProcedureColumnMetaData = context.isAccessCallParameterMetaData();
if (context.isFunction()) {
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -49,9 +49,8 @@ private TableMetaDataProviderFactory() {
*/
public static TableMetaDataProvider createMetaDataProvider(DataSource dataSource, TableMetaDataContext context) {
try {
return (TableMetaDataProvider) JdbcUtils.extractDatabaseMetaData(dataSource, databaseMetaData -> {
String databaseProductName =
JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName());
return JdbcUtils.extractDatabaseMetaData(dataSource, databaseMetaData -> {
String databaseProductName = JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName());
boolean accessTableColumnMetaData = context.isAccessTableColumnMetaData();
TableMetaDataProvider provider;

Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -26,10 +26,12 @@
* and handled correctly by the JdbcUtils class.
*
* @author Thomas Risberg
* @see JdbcUtils#extractDatabaseMetaData
* @author Juergen Hoeller
* @param <T> the result type
* @see JdbcUtils#extractDatabaseMetaData(javax.sql.DataSource, DatabaseMetaDataCallback)
*/
@FunctionalInterface
public interface DatabaseMetaDataCallback {
public interface DatabaseMetaDataCallback<T> {

/**
* Implementations must implement this method to process the meta-data
Expand All @@ -42,6 +44,6 @@ public interface DatabaseMetaDataCallback {
* @throws MetaDataAccessException in case of other failures while
* extracting meta-data (for example, reflection failure)
*/
Object processMetaData(DatabaseMetaData dbmd) throws SQLException, MetaDataAccessException;
T processMetaData(DatabaseMetaData dbmd) throws SQLException, MetaDataAccessException;

}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -315,26 +315,44 @@ else if (obj instanceof java.sql.Date) {

/**
* Extract database meta-data via the given DatabaseMetaDataCallback.
* <p>This method will open a connection to the database and retrieve the database meta-data.
* Since this method is called before the exception translation feature is configured for
* a datasource, this method can not rely on the SQLException translation functionality.
* <p>Any exceptions will be wrapped in a MetaDataAccessException. This is a checked exception
* and any calling code should catch and handle this exception. You can just log the
* error and hope for the best, but there is probably a more serious error that will
* reappear when you try to access the database again.
* <p>This method will open a connection to the database and retrieve its meta-data.
* Since this method is called before the exception translation feature is configured
* for a DataSource, this method can not rely on SQLException translation itself.
* <p>Any exceptions will be wrapped in a MetaDataAccessException. This is a checked
* exception and any calling code should catch and handle this exception. You can just
* log the error and hope for the best, but there is probably a more serious error that
* will reappear when you try to access the database again.
* @param dataSource the DataSource to extract meta-data for
* @param action callback that will do the actual work
* @return object containing the extracted information, as returned by
* the DatabaseMetaDataCallback's {@code processMetaData} method
* @throws MetaDataAccessException if meta-data access failed
* @see java.sql.DatabaseMetaData
*/
public static Object extractDatabaseMetaData(DataSource dataSource, DatabaseMetaDataCallback action)
public static <T> T extractDatabaseMetaData(DataSource dataSource, DatabaseMetaDataCallback<T> action)
throws MetaDataAccessException {

Connection con = null;
try {
con = DataSourceUtils.getConnection(dataSource);
DatabaseMetaData metaData = con.getMetaData();
DatabaseMetaData metaData;
try {
metaData = con.getMetaData();
}
catch (SQLException ex) {
if (DataSourceUtils.isConnectionTransactional(con, dataSource)) {
// Probably a closed thread-bound Connection - retry against fresh Connection
DataSourceUtils.releaseConnection(con, dataSource);
con = null;
logger.debug("Failed to obtain DatabaseMetaData from transactional Connection - " +
"retrying against fresh Connection", ex);
con = dataSource.getConnection();
metaData = con.getMetaData();
}
else {
throw ex;
}
}
if (metaData == null) {
// should only happen in test environments
throw new MetaDataAccessException("DatabaseMetaData returned by Connection [" + con + "] was null");
Expand Down Expand Up @@ -365,7 +383,11 @@ public static Object extractDatabaseMetaData(DataSource dataSource, DatabaseMeta
* @throws MetaDataAccessException if we couldn't access the DatabaseMetaData
* or failed to invoke the specified method
* @see java.sql.DatabaseMetaData
* @deprecated as of 5.2.9, in favor of
* {@link #extractDatabaseMetaData(DataSource, DatabaseMetaDataCallback)}
* with a lambda expression or method reference and a generically typed result
*/
@Deprecated
@SuppressWarnings("unchecked")
public static <T> T extractDatabaseMetaData(DataSource dataSource, final String metaDataMethodName)
throws MetaDataAccessException {
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -35,6 +35,8 @@
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.InvalidResultSetAccessException;
import org.springframework.lang.Nullable;
import org.springframework.util.function.SingletonSupplier;
import org.springframework.util.function.SupplierUtils;

/**
* Implementation of {@link SQLExceptionTranslator} that analyzes vendor-specific error codes.
Expand Down Expand Up @@ -76,7 +78,7 @@ public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExcep

/** Error codes used by this translator. */
@Nullable
private SQLErrorCodes sqlErrorCodes;
private SingletonSupplier<SQLErrorCodes> sqlErrorCodes;


/**
Expand Down Expand Up @@ -120,7 +122,7 @@ public SQLErrorCodeSQLExceptionTranslator(String dbName) {
*/
public SQLErrorCodeSQLExceptionTranslator(SQLErrorCodes sec) {
this();
this.sqlErrorCodes = sec;
this.sqlErrorCodes = SingletonSupplier.of(sec);
}


Expand All @@ -134,7 +136,9 @@ public SQLErrorCodeSQLExceptionTranslator(SQLErrorCodes sec) {
* @see java.sql.DatabaseMetaData#getDatabaseProductName()
*/
public void setDataSource(DataSource dataSource) {
this.sqlErrorCodes = SQLErrorCodesFactory.getInstance().getErrorCodes(dataSource);
this.sqlErrorCodes =
SingletonSupplier.of(() -> SQLErrorCodesFactory.getInstance().resolveErrorCodes(dataSource));
this.sqlErrorCodes.get(); // try early initialization - otherwise the supplier will retry later
}

/**
Expand All @@ -146,15 +150,15 @@ public void setDataSource(DataSource dataSource) {
* @see java.sql.DatabaseMetaData#getDatabaseProductName()
*/
public void setDatabaseProductName(String dbName) {
this.sqlErrorCodes = SQLErrorCodesFactory.getInstance().getErrorCodes(dbName);
this.sqlErrorCodes = SingletonSupplier.of(SQLErrorCodesFactory.getInstance().getErrorCodes(dbName));
}

/**
* Set custom error codes to be used for translation.
* @param sec custom error codes to use
*/
public void setSqlErrorCodes(@Nullable SQLErrorCodes sec) {
this.sqlErrorCodes = sec;
this.sqlErrorCodes = SingletonSupplier.ofNullable(sec);
}

/**
Expand All @@ -164,7 +168,7 @@ public void setSqlErrorCodes(@Nullable SQLErrorCodes sec) {
*/
@Nullable
public SQLErrorCodes getSqlErrorCodes() {
return this.sqlErrorCodes;
return SupplierUtils.resolve(this.sqlErrorCodes);
}


Expand All @@ -175,7 +179,6 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL
if (sqlEx instanceof BatchUpdateException && sqlEx.getNextException() != null) {
SQLException nestedSqlEx = sqlEx.getNextException();
if (nestedSqlEx.getErrorCode() > 0 || nestedSqlEx.getSQLState() != null) {
logger.debug("Using nested SQLException from the BatchUpdateException");
sqlEx = nestedSqlEx;
}
}
Expand All @@ -187,8 +190,9 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL
}

// Next, try the custom SQLException translator, if available.
if (this.sqlErrorCodes != null) {
SQLExceptionTranslator customTranslator = this.sqlErrorCodes.getCustomSqlExceptionTranslator();
SQLErrorCodes sqlErrorCodes = getSqlErrorCodes();
if (sqlErrorCodes != null) {
SQLExceptionTranslator customTranslator = sqlErrorCodes.getCustomSqlExceptionTranslator();
if (customTranslator != null) {
DataAccessException customDex = customTranslator.translate(task, sql, sqlEx);
if (customDex != null) {
Expand All @@ -198,9 +202,9 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL
}

// Check SQLErrorCodes with corresponding error code, if available.
if (this.sqlErrorCodes != null) {
if (sqlErrorCodes != null) {
String errorCode;
if (this.sqlErrorCodes.isUseSqlStateForTranslation()) {
if (sqlErrorCodes.isUseSqlStateForTranslation()) {
errorCode = sqlEx.getSQLState();
}
else {
Expand All @@ -215,7 +219,7 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL

if (errorCode != null) {
// Look for defined custom translations first.
CustomSQLErrorCodesTranslation[] customTranslations = this.sqlErrorCodes.getCustomTranslations();
CustomSQLErrorCodesTranslation[] customTranslations = sqlErrorCodes.getCustomTranslations();
if (customTranslations != null) {
for (CustomSQLErrorCodesTranslation customTranslation : customTranslations) {
if (Arrays.binarySearch(customTranslation.getErrorCodes(), errorCode) >= 0 &&
Expand All @@ -230,43 +234,43 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL
}
}
// Next, look for grouped error codes.
if (Arrays.binarySearch(this.sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) {
if (Arrays.binarySearch(sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx);
}
else if (Arrays.binarySearch(this.sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) {
else if (Arrays.binarySearch(sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new InvalidResultSetAccessException(task, (sql != null ? sql : ""), sqlEx);
}
else if (Arrays.binarySearch(this.sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) {
else if (Arrays.binarySearch(sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if (Arrays.binarySearch(this.sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) {
else if (Arrays.binarySearch(sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if (Arrays.binarySearch(this.sqlErrorCodes.getPermissionDeniedCodes(), errorCode) >= 0) {
else if (Arrays.binarySearch(sqlErrorCodes.getPermissionDeniedCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new PermissionDeniedDataAccessException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if (Arrays.binarySearch(this.sqlErrorCodes.getDataAccessResourceFailureCodes(), errorCode) >= 0) {
else if (Arrays.binarySearch(sqlErrorCodes.getDataAccessResourceFailureCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new DataAccessResourceFailureException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if (Arrays.binarySearch(this.sqlErrorCodes.getTransientDataAccessResourceCodes(), errorCode) >= 0) {
else if (Arrays.binarySearch(sqlErrorCodes.getTransientDataAccessResourceCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new TransientDataAccessResourceException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotAcquireLockCodes(), errorCode) >= 0) {
else if (Arrays.binarySearch(sqlErrorCodes.getCannotAcquireLockCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if (Arrays.binarySearch(this.sqlErrorCodes.getDeadlockLoserCodes(), errorCode) >= 0) {
else if (Arrays.binarySearch(sqlErrorCodes.getDeadlockLoserCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new DeadlockLoserDataAccessException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotSerializeTransactionCodes(), errorCode) >= 0) {
else if (Arrays.binarySearch(sqlErrorCodes.getCannotSerializeTransactionCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new CannotSerializeTransactionException(buildMessage(task, sql, sqlEx), sqlEx);
}
Expand All @@ -276,7 +280,7 @@ else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotSerializeTransactionCod
// We couldn't identify it more precisely - let's hand it over to the SQLState fallback translator.
if (logger.isDebugEnabled()) {
String codes;
if (this.sqlErrorCodes != null && this.sqlErrorCodes.isUseSqlStateForTranslation()) {
if (sqlErrorCodes != null && sqlErrorCodes.isUseSqlStateForTranslation()) {
codes = "SQL state '" + sqlEx.getSQLState() + "', error code '" + sqlEx.getErrorCode();
}
else {
Expand Down

0 comments on commit 670b9fd

Please sign in to comment.