Skip to content

PLUGIN-1823: Retrying all SQLTransientExceptions #597

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from

Conversation

sgarg-CS
Copy link
Contributor

@sgarg-CS sgarg-CS commented May 16, 2025

PLUGIN-1823

Add Failsafe Retry poilcy to all the places in the database-plugins where SQLTransientException could be thrown.

Added three new properties (hidden from UI)

  • Initial Retry Duration (Default: 5sec)
  • Max Retry Duration (Default: 80 sec)
  • Max Retry Count (Default: 5)

Copy link

google-cla bot commented May 16, 2025

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@sgarg-CS sgarg-CS force-pushed the patch/plugin-1823 branch from 9da75b8 to ac813f0 Compare May 26, 2025 05:28
@sgarg-CS sgarg-CS added build and removed build labels May 27, 2025
@sgarg-CS sgarg-CS requested a review from itsankit-google May 30, 2025 04:56
Copy link
Member

@itsankit-google itsankit-google left a comment

Choose a reason for hiding this comment

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

Overall, this looks everything is getting wrapped within Failsafe where we might end up with having nested level retries, we need to ensure we add retries only where we are actually interacting with JDBC client and not top level functions.

For example adding retries to DriverManager.getConnection(connectionString, connectionProperties) makes sense because you are actually interacting with the source db but adding retries to whole loadSchema(Connection connection, String query) do not makes sense we need to be careful while adding such retries.

@@ -48,37 +50,46 @@ public DBRun(QueryConfig config, Class<? extends Driver> driverClass, Boolean en
* to use and which connection string to use come from the plugin configuration.
*/
public void run() throws SQLException, InstantiationException, IllegalAccessException {

Copy link
Member

Choose a reason for hiding this comment

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

nit: remove empty line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

Comment on lines 205 to 227
catch (SQLException e) {
// wrap exception to ensure SQLException-child instances not exposed to contexts without jdbc
// driver in classpath
String errorMessage =
String.format("SQL Exception occurred: [Message='%s', SQLState='%s', ErrorCode='%s'].",
e.getMessage(),
e.getSQLState(), e.getErrorCode());
String errorMessageWithDetails = String.format("Error occurred while trying to" +
" get schema from database." +
"Error message: '%s'. Error code: '%s'. SQLState: '%s'", e.getMessage(), e.getErrorCode(),
e.getSQLState());
String externalDocumentationLink = getExternalDocumentationLink();
if (!Strings.isNullOrEmpty(externalDocumentationLink)) {
if (!errorMessage.endsWith(".")) {
errorMessage = errorMessage + ".";
}
errorMessage = String.format("%s For more details, see %s", errorMessage,
externalDocumentationLink);
}
throw ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
errorMessage, errorMessageWithDetails, ErrorType.USER, false, ErrorCodeType.SQLSTATE,
e.getSQLState(), externalDocumentationLink, new SQLException(e.getMessage(),
e.getSQLState(), e.getErrorCode()));
Copy link
Member

Choose a reason for hiding this comment

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

If loadSchemaFromDB(Connection connection, String query) already includes retry logic, we don't need to add another layer here, as having nested retries can lead to undesirable behavior.

Also Failsafe will wrap any non-retryable exception or an exception after all retries are exhausted in a FailsafeException so where ever we are doing catch(SQLException) it won't work, you might need to catch FailsafeException & check cause.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the nested retry logic. Also, handled the FailsafeException and the wrapped exceptions in a separate method : unwrapFailsafeException() of RetryUtils class as suggested in the other comment.

@itsankit-google
Copy link
Member

Please note E2E should not be modified and not fail with these changes. Otherwise, we have done something wrong which does not give expected failure messages.

errorMessage, errorMessageWithDetails, ErrorType.USER, false, ErrorCodeType.SQLSTATE,
e.getSQLState(), externalDocumentationLink, new SQLException(e.getMessage(),
e.getSQLState(), e.getErrorCode()));
throw e;
Copy link
Member

Choose a reason for hiding this comment

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

we should throw cause instead of FailsafeException.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Handled the FailsafeException in a separate method : unwrapFailsafeException of RetryUtils class as suggested in the other comment.

}
throw e;
Copy link
Member

Choose a reason for hiding this comment

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

we should throw cause instead of FailsafeException.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Handled the FailsafeException in a separate method : unwrapFailsafeException of RetryUtils class as suggested in the other comment.

errorMessage, errorMessageWithDetails, ErrorType.USER, false, ErrorCodeType.SQLSTATE,
e.getSQLState(), externalDocumentationLink, new SQLException(e.getMessage(),
e.getSQLState(), e.getErrorCode()));
throw e;
Copy link
Member

Choose a reason for hiding this comment

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

we should throw cause instead of FailsafeException.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Handled the FailsafeException in a separate method : unwrapFailsafeException of RetryUtils class as suggested in the other comment.

Comment on lines 290 to 295
return Failsafe.with(retryPolicy).<Connection>get(() -> DriverManager
.getConnection(connectionString, connectionProperties));
}

private Statement createStatementWithRetry(Connection connection) {
return Failsafe.with(retryPolicy).<Statement>get(() -> connection.createStatement());
Copy link
Member

Choose a reason for hiding this comment

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

we should throw cause instead of FailsafeException.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Handled the FailsafeException in a separate method : unwrapFailsafeException of RetryUtils class as suggested in the other comment.

Copy link
Member

@itsankit-google itsankit-google left a comment

Choose a reason for hiding this comment

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

I can see some level of duplication in both AbstractDBSource & AbstractDBSink, can we please move it to the common AbstractDBUtil class?

return maxRetryCount == null ? DEFAULT_MAX_RETRY_COUNT : maxRetryCount;
}


Copy link
Member

Choose a reason for hiding this comment

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

remove extra empty line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

Comment on lines 182 to 184
ResultSet resultSet = Failsafe.with(RetryPolicyUtil.getRetryPolicy(config.getInitialRetryDuration(),
config.getMaxRetryDuration(), config.getMaxRetryCount()))
.get(() -> statement.executeQuery(query));
Copy link
Member

Choose a reason for hiding this comment

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

we should not throw FailsafeException

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Handled the FailsafeException in a separate method : unwrapFailsafeException of RetryUtils class as suggested in the other comment.

Copy link
Member

@itsankit-google itsankit-google left a comment

Choose a reason for hiding this comment

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

public final class RetryUtils {

  public static Connection createConnectionWithRetry(RetryPolicy<?> retryPolicy, String connectionString,
                                                     Properties connectionProperties, String externalDocumentationLink) throws Exception {
    try {
      return Failsafe.with(retryPolicy).get(() ->
        DriverManager.getConnection(connectionString, connectionProperties)
      );
    } catch (Exception e) {
      throw unwrapFailsafeException(e, externalDocumentationLink);
    }
  }

  public static Statement createStatementWithRetry(RetryPolicy<?> retryPolicy,
                                                   Connection connection, String externalDocumentationLink) throws Exception {
    try {
      return Failsafe.with(retryPolicy).get(connection::createStatement);
    } catch (Exception e) {
      throw unwrapFailsafeException(e, externalDocumentationLink);
    }
  }

  public static PreparedStatement prepareStatementWithRetry(RetryPolicy<?> retryPolicy,
                                                            Connection connection,
                                                            String sqlQuery, String externalDocumentationLink) throws Exception {
    try {
      return Failsafe.with(retryPolicy).get(() ->
        connection.prepareStatement(sqlQuery)
      );
    } catch (Exception e) {
      throw unwrapFailsafeException(e, externalDocumentationLink);
    }
  }

 public static ResultSet executeWithRetry(RetryPolicy<?> retryPolicy,
                                                            Connection connection,
                                                            String sqlQuery, String externalDocumentationLink) throws Exception {
        try {
            return Failsafe.with(retryPolicy).get(() -> connection.createStatement().executeQuery(sqlQuery));
        } catch (Exception e) {
            throw unwrapFailsafeException(e, externalDocumentationLink);
        }
    }

 private static Exception unwrapFailsafeException(Exception e) {
    if (e instanceof FailsafeException && e.getCause() instanceof Exception) {
        if (e instanceOf SQLException) {
           return programFailureException(e, externalDocumentationLink);
        } else {
          return (Exception) e.getCause();
       }
    }
    return e;
  }
  
private static ProgramFailureException programFailureException(SQLException e, String externalDocumentationLink) {
    // wrap exception to ensure SQLException-child instances not exposed to contexts without jdbc
    // driver in classpath
    String errorMessage =
      String.format("SQL Exception occurred: [Message='%s', SQLState='%s', ErrorCode='%s'].",
        e.getMessage(), e.getSQLState(), e.getErrorCode());
    String errorMessageWithDetails = String.format("Error occurred while trying to" +
      " get schema from database." + "Error message: '%s'. Error code: '%s'. SQLState: '%s'", e.getMessage(),
        e.getErrorCode(), e.getSQLState());
 
    if (!Strings.isNullOrEmpty(externalDocumentationLink)) {
      if (!errorMessage.endsWith(".")) {
        errorMessage = errorMessage + ".";
      }
      errorMessage = String.format("%s For more details, see %s", errorMessage, externalDocumentationLink);
    }
    return ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
      errorMessage, errorMessageWithDetails, ErrorType.USER, false, ErrorCodeType.SQLSTATE, e.getSQLState(),
        externalDocumentationLink, e);
  }
}

You can create a RetryUtils like above which accepts connection params.


protected final T sourceConfig;
protected Class<? extends Driver> driverClass;


Copy link
Member

Choose a reason for hiding this comment

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

remove extra empty line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

Move retry logic into a separate class: RetryUtils and add exception handling
@sgarg-CS
Copy link
Contributor Author

sgarg-CS commented Jun 3, 2025

Overall, this looks everything is getting wrapped within Failsafe where we might end up with having nested level retries, we need to ensure we add retries only where we are actually interacting with JDBC client and not top level functions.

For example adding retries to DriverManager.getConnection(connectionString, connectionProperties) makes sense because you are actually interacting with the source db but adding retries to whole loadSchema(Connection connection, String query) do not makes sense we need to be careful while adding such retries.

Refactored the code to add the retry logic only for the methods interacting with the JDBC client.

Comment on lines 222 to 242
private ProgramFailureException wrapException(SQLException e) {
// wrap exception to ensure SQLException-child instances not exposed to contexts without jdbc
// driver in classpath
String errorMessage =
String.format("SQL Exception occurred: [Message='%s', SQLState='%s', ErrorCode='%s'].",
e.getMessage(), e.getSQLState(), e.getErrorCode());
String errorMessageWithDetails = String.format("Error occurred while trying to" +
" get schema from database." + "Error message: '%s'. Error code: '%s'. SQLState: '%s'", e.getMessage(),
e.getErrorCode(), e.getSQLState());
String externalDocumentationLink = getExternalDocumentationLink();
if (!Strings.isNullOrEmpty(externalDocumentationLink)) {
if (!errorMessage.endsWith(".")) {
errorMessage = errorMessage + ".";
}
errorMessage = String.format("%s For more details, see %s", errorMessage, externalDocumentationLink);
}
return ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
errorMessage, errorMessageWithDetails, ErrorType.USER, false, ErrorCodeType.SQLSTATE, e.getSQLState(),
externalDocumentationLink, new SQLException(e.getMessage(), e.getSQLState(), e.getErrorCode()));

}
Copy link
Member

Choose a reason for hiding this comment

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

where this method used now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's unused now. Removed it. fc78d9c

errorMessage = String.format("%s For more details, see %s", errorMessage, externalDocumentationLink);
}
return ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
errorMessage, errorMessageWithDetails, ErrorType.USER, false, ErrorCodeType.SQLSTATE, e.getSQLState(),
Copy link
Member

Choose a reason for hiding this comment

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

this cannot be USER always, we should check based on SQLState and dependency should be true. Not sure how was this missed earlier.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you give some pointers where to fetch all possible SQL States?

Should it be something like this?

String sqlState = e.getSQLState();
ErrorType errorType = getErrorTypeFromSQLState(sqlState);
private static ErrorType getErrorTypeFromSQLState(String sqlState) {
    if (sqlState != null && sqlState.length() >= 2) {
        String classCode = sqlState.substring(0, 2);
        switch (classCode) {
            case "22": // Data Exception
            case "23": // Integrity Constraint Violation
            case "28": // Invalid Authorization
            case "42": // Syntax Error or Access Rule Violation
                return ErrorType.USER;
            case "08": // Connection Exception
            case "53": // Insufficient Resources
            case "57": // Operator Intervention
            case "58": // System Error
                return ErrorType.SYSTEM;
            default:
                // Check for the timeout codes
                if ("HYT00".equals(sqlState) || "HYT01".equals(sqlState)) {
                    return ErrorType.SYSTEM;
                }
                break;
        }
    }
    return ErrorType.UNKNOWN;
}

Copy link
Member

@itsankit-google itsankit-google Jun 4, 2025

Choose a reason for hiding this comment

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

you can directly use DBErrorDetailsProvider#getException#getProgramFailureException : https://github.com/cdapio/hydrator-plugins/blob/b9723d1b24068347c60ad2b0489064902f335de9/hydrator-common/src/main/java/io/cdap/plugin/common/db/DBErrorDetailsProvider.java#L184

create an object of DBErrorDetailsProvider in RetryUtils

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we directly use this method, then we wouldn't be able to pass the externalDocumentationLink as an argument in the method call to DBErrorDetailsProvider#getException#getProgramFailureException unless we create another overloaded method in the DBErrorDetailsProvider class to accept this new parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did some more code analysis to find out that there's an alternate approach we can try where we'd need to create a method to return the instance of DBErrorDetailsProvider in AbstractSource/AbstractSink classes and then override this method in the respective connectors' subclasses to return the instance of sub-classes of DBErrorDetailsProvider and then call the below methods for

  1. getExternalDocumentationLink()
  2. getErrorTypeFromErrorCodeAndSqlState() and
  3. getErrorCategoryFromSqlState()
    using the returned instance

AbstractDBSource

protected DBErrorDetailsProvider getDBErrorDetailsProvider() {
    return new DBErrorDetailsProvider();
}

PostgresSource

@Override
protected PostgresErrorDetailsProvider getDBErrorDetailsProvider() {
    return new PostgresErrorDetailsProvider();
}

To go with this approach, we'd need to make the access-modifier for the 3 methods mentioned above, from protected to public in the DBErrorDetailsProvider class because Java uses static types at compile time. (Even though we can increase the visibility for the overriden methods from protected to public and JVM will resolve to the sub-class overridden methods for respective connectors at the run time)

Now, if we need to avoid this change for the access modifier, it will additionally require us to create methods for all the 3 methods mentioned above, in AbstractSource/AbstractSink and then override them with the connector specific implementation such as this

AbstractDBSource

protected String getExternalDocumentationLink() {
    return "https://en.wikipedia.org/wiki/SQLSTATE"; //Default
}

PostgresSource

@Override 
protected String getExternalDocumentationLink() {
    return getDBErrorDetailsProvider().getExternalDocumentationLink();
}

This will be a significant change and may stretch this PR further.

Comment on lines 39 to 44
private static final String NAME_INITIAL_RETRY_DURATION = "initialRetryDuration";
private static final String NAME_MAX_RETRY_DURATION = "maxRetryDuration";
private static final String NAME_MAX_RETRY_COUNT = "maxRetryCount";
public static final int DEFAULT_INITIAL_RETRY_DURATION_SECONDS = 5;
public static final int DEFAULT_MAX_RETRY_COUNT = 5;
public static final int DEFAULT_MAX_RETRY_DURATION_SECONDS = 80;
Copy link
Member

@itsankit-google itsankit-google Jun 4, 2025

Choose a reason for hiding this comment

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

move all of this to RetryPolicyUtil and use it everywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved the constants from AbstractDBConnectorConfig and ConnectionConfig classes to RetryUtils class 037a0d4

Comment on lines 232 to 234
} catch (SQLException e) {
throw new RuntimeException(String.format("Unable to validate schema due to: %s.",
ExceptionUtils.getRootCauseMessage(e)), e);
Copy link
Member

Choose a reason for hiding this comment

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

when it will be thrown if RetryUtils will always throw ProgramFailureException for every SQLException?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The try-with-resources automatically closes any resources that implement AutoCloseable (like Connection, Statement, or ResultSet)

This SQLException is thrown from the auto-closeable resource when we perform any SQL related operation such as creating a connection or creating statement in the try-with-resources block. If we remove the try-wth-resources, then the SQLException is not thrown

In case we want to keep the try-with-resources, then either we need to add throws SQLException (or Exception) in the enclosing method signature OR we need to catch the SQLException and handle it.

@@ -168,12 +168,12 @@ public Schema getSchema() throws SQLException {
}

private Schema loadSchemaFromDB(Connection connection, String query) throws SQLException {
Statement statement = connection.createStatement();
Statement statement = RetryUtils.createStatementWithRetry((RetryPolicy<Statement>) retryPolicy, connection,
Copy link
Member

Choose a reason for hiding this comment

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

can we use try with resources here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, added try-with-resource. 037a0d4

Copy link
Member

@itsankit-google itsankit-google left a comment

Choose a reason for hiding this comment

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

How does it affect the UI if validateSchema fails for DB sinks? Can we test before/after the change?

Moved all the retry constants to RetryUtils class, Added the methods to determine error type and error category methods from Error code and SQL State
@sgarg-CS
Copy link
Contributor Author

sgarg-CS commented Jun 5, 2025

How does it affect the UI if validateSchema fails for DB sinks? Can we test before/after the change?

Yes. Will check and update the behaviour here.

I've reverted the changes done to handle SQLException thrown by validateSchema() method call from AbstractDBSink.configurePipeline class. Still, I see a change in the error message on the UI. This is probably due to catching SQLException and then wrapping it to throw ProgramFailureException. While this wrapping was done for inferSchema() but not for validateSchema() in AbstractSink class earlier.

Test Scenario: Validate the schema in PostgreSQL Sink Plugin, if connection is not active. (Postgres DB is down)

[BEFORE CHANGES]

Error message on the UI:
Exception while trying to validate schema of database table '"users2"' for connection 'jdbc:postgresql://localhost:5433/postgres' with Connection to localhost:5433 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.

[AFTER CHANGES]

Error message on the UI:
Error encountered while configuring the stage: 'Error occurred while trying to get schema from database. Error message: 'Connection to localhost:5433 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.'. Error code: '0'. SQLState: '08001''

Is the new error message acceptable or should we revert the changes done to throw ProgramFailureException for validateSchema() method?

@itsankit-google
Copy link
Member

How does it affect the UI if validateSchema fails for DB sinks? Can we test before/after the change?

Yes. Will check and update the behaviour here.

I've reverted the changes done to handle SQLException thrown by validateSchema() method call from AbstractDBSink.configurePipeline class. Still, I see a change in the error message on the UI. This is probably due to catching SQLException and then wrapping it to throw ProgramFailureException. While this wrapping was done for inferSchema() but not for validateSchema() in AbstractSink class earlier.

Test Scenario: Validate the schema in PostgreSQL Sink Plugin, if connection is not active. (Postgres DB is down)

[BEFORE CHANGES]

Error message on the UI: Exception while trying to validate schema of database table '"users2"' for connection 'jdbc:postgresql://localhost:5433/postgres' with Connection to localhost:5433 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.

[AFTER CHANGES]

Error message on the UI: Error encountered while configuring the stage: 'Error occurred while trying to get schema from database. Error message: 'Connection to localhost:5433 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.'. Error code: '0'. SQLState: '08001''

Is the new error message acceptable or should we revert the changes done to throw ProgramFailureException for validateSchema() method?

LGTM

Comment on lines +53 to +173
ERROR_CODE_TO_ERROR_TYPE.put("0W", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("0X", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("0Y", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("0Z", ErrorType.SYSTEM);
ERROR_CODE_TO_ERROR_TYPE.put("10", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("20", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("21", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("22", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("23", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("24", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("25", ErrorType.SYSTEM);
ERROR_CODE_TO_ERROR_TYPE.put("26", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("27", ErrorType.SYSTEM);
ERROR_CODE_TO_ERROR_TYPE.put("28", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("2B", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("2C", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("2D", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("2E", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("2F", ErrorType.SYSTEM);
ERROR_CODE_TO_ERROR_TYPE.put("2H", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("30", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("33", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("34", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("35", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("36", ErrorType.SYSTEM);
ERROR_CODE_TO_ERROR_TYPE.put("38", ErrorType.SYSTEM);
ERROR_CODE_TO_ERROR_TYPE.put("39", ErrorType.SYSTEM);
ERROR_CODE_TO_ERROR_TYPE.put("3B", ErrorType.SYSTEM);
ERROR_CODE_TO_ERROR_TYPE.put("3C", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("3D", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("3F", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("40", ErrorType.SYSTEM);
ERROR_CODE_TO_ERROR_TYPE.put("42", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("44", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("45", ErrorType.USER);
ERROR_CODE_TO_ERROR_TYPE.put("46", ErrorType.SYSTEM);
ERROR_CODE_TO_ERROR_TYPE.put("HW", ErrorType.SYSTEM);

ERROR_CODE_TO_ERROR_CATEGORY = new HashMap<>();
ErrorCategory.ErrorCategoryEnum plugin = ErrorCategory.ErrorCategoryEnum.PLUGIN;
ERROR_CODE_TO_ERROR_CATEGORY.put("01", new ErrorCategory(plugin, "DB Warning"));
ERROR_CODE_TO_ERROR_CATEGORY.put("02", new ErrorCategory(plugin, "DB No Data"));
ERROR_CODE_TO_ERROR_CATEGORY.put("07", new ErrorCategory(plugin, "DB Dynamic SQL error"));
ERROR_CODE_TO_ERROR_CATEGORY.put("08", new ErrorCategory(plugin, "DB Connection Exception"));
ERROR_CODE_TO_ERROR_CATEGORY.put("09", new ErrorCategory(plugin, "DB Triggered Action Exception"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0A", new ErrorCategory(plugin, "DB Feature Not Supported"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0D", new ErrorCategory(plugin, "DB Invalid Target Type Specification"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0E", new ErrorCategory(plugin, "DB Invalid Schema Name List Specification"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0F", new ErrorCategory(plugin, "DB Locator Exception"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0K", new ErrorCategory(plugin, "DB Resignal When Handler Not Active"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0L", new ErrorCategory(plugin, "DB Invalid Grantor"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0M", new ErrorCategory(plugin, "DB Invalid SQL-Invoked Procedure Reference"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0N", new ErrorCategory(plugin, "DB SQL/XML Mapping Error"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0P", new ErrorCategory(plugin, "DB Invalid Role Specification"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0S",
new ErrorCategory(plugin, "DB Invalid Transform Group Name Specification"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0T",
new ErrorCategory(plugin, "DB Target Table Disagrees With Cursor Specification"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0U",
new ErrorCategory(plugin, "DB Attempt To Assign To Non-Updatable Column"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0V", new ErrorCategory(plugin, "DB Attempt To Assign To Ordering Column"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0W", new ErrorCategory(plugin, "DB Prohibited Statement Encountered"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0X", new ErrorCategory(plugin, "DB Invalid Foreign Server Specification"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0Y", new ErrorCategory(plugin, "DB Pass-Through Specific Condition"));
ERROR_CODE_TO_ERROR_CATEGORY.put("0Z", new ErrorCategory(plugin, "DB Diagnostics Exception"));
ERROR_CODE_TO_ERROR_CATEGORY.put("10", new ErrorCategory(plugin, "DB XQuery Error"));
ERROR_CODE_TO_ERROR_CATEGORY.put("20", new ErrorCategory(plugin, "DB Case Not Found For Case Statement"));
ERROR_CODE_TO_ERROR_CATEGORY.put("21", new ErrorCategory(plugin, "DB Cardinality Violation"));
ERROR_CODE_TO_ERROR_CATEGORY.put("22", new ErrorCategory(plugin, "DB Data Exception"));
ERROR_CODE_TO_ERROR_CATEGORY.put("23", new ErrorCategory(plugin, "DB Integrity Constraint Violation"));
ERROR_CODE_TO_ERROR_CATEGORY.put("24", new ErrorCategory(plugin, "DB Invalid Cursor State"));
ERROR_CODE_TO_ERROR_CATEGORY.put("25", new ErrorCategory(plugin, "DB Invalid Transaction State"));
ERROR_CODE_TO_ERROR_CATEGORY.put("26", new ErrorCategory(plugin, "DB Invalid SQL Statement Name"));
ERROR_CODE_TO_ERROR_CATEGORY.put("27", new ErrorCategory(plugin, "DB Triggered Data Change Violation"));
ERROR_CODE_TO_ERROR_CATEGORY.put("28", new ErrorCategory(plugin, "DB Invalid Authorization Specification"));
ERROR_CODE_TO_ERROR_CATEGORY.put("2B",
new ErrorCategory(plugin, "DB Dependent Privilege Descriptors Still Exist"));
ERROR_CODE_TO_ERROR_CATEGORY.put("2C", new ErrorCategory(plugin, "DB Invalid Character Set Name"));
ERROR_CODE_TO_ERROR_CATEGORY.put("2D", new ErrorCategory(plugin, "DB Invalid Transaction Termination"));
ERROR_CODE_TO_ERROR_CATEGORY.put("2E", new ErrorCategory(plugin, "DB Invalid Connection Name"));
ERROR_CODE_TO_ERROR_CATEGORY.put("2F", new ErrorCategory(plugin, "DB SQL Routine Exception"));
ERROR_CODE_TO_ERROR_CATEGORY.put("2H", new ErrorCategory(plugin, "DB Invalid Collation Name"));
ERROR_CODE_TO_ERROR_CATEGORY.put("30", new ErrorCategory(plugin, "DB Invalid SQL Statement Identifier"));
ERROR_CODE_TO_ERROR_CATEGORY.put("33", new ErrorCategory(plugin, "DB Invalid SQL Descriptor Name"));
ERROR_CODE_TO_ERROR_CATEGORY.put("34", new ErrorCategory(plugin, "DB Invalid Cursor Name"));
ERROR_CODE_TO_ERROR_CATEGORY.put("35", new ErrorCategory(plugin, "DB Invalid Condition Number"));
ERROR_CODE_TO_ERROR_CATEGORY.put("36", new ErrorCategory(plugin, "DB Cursor Sensitivity Exception"));
ERROR_CODE_TO_ERROR_CATEGORY.put("38", new ErrorCategory(plugin, "DB External Routine Exception"));
ERROR_CODE_TO_ERROR_CATEGORY.put("39", new ErrorCategory(plugin, "DB External Routine Invocation Exception"));
ERROR_CODE_TO_ERROR_CATEGORY.put("3B", new ErrorCategory(plugin, "DB Savepoint Exception"));
ERROR_CODE_TO_ERROR_CATEGORY.put("3C", new ErrorCategory(plugin, "DB Ambiguous Cursor Name"));
ERROR_CODE_TO_ERROR_CATEGORY.put("3D", new ErrorCategory(plugin, "DB Invalid Catalog Name"));
ERROR_CODE_TO_ERROR_CATEGORY.put("3F", new ErrorCategory(plugin, "DB Invalid Schema Name"));
ERROR_CODE_TO_ERROR_CATEGORY.put("40", new ErrorCategory(plugin, "DB Transaction Rollback"));
ERROR_CODE_TO_ERROR_CATEGORY.put("42", new ErrorCategory(plugin, "DB Syntax Error or Access Rule Violation"));
ERROR_CODE_TO_ERROR_CATEGORY.put("44", new ErrorCategory(plugin, "DB With Check Option Violation"));
ERROR_CODE_TO_ERROR_CATEGORY.put("45", new ErrorCategory(plugin, "DB Unhandled User-Defined Exception"));
ERROR_CODE_TO_ERROR_CATEGORY.put("46", new ErrorCategory(plugin, "DB JAVA DDL"));
ERROR_CODE_TO_ERROR_CATEGORY.put("HW", new ErrorCategory(plugin, "DB Datalink Exception"));
}
Copy link
Member

Choose a reason for hiding this comment

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

this is not meant to be copied and maintained at two places.

We can just create an object of DBErrorDetailsProvider

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Those maps are private and there are not public getter methods for them in the DBErrorDetailsProvider. Are you saying that we can modify the class and raise a PR in thehydrator-plugins repo?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This class:

does make use of some of the error codes

Copy link
Member

Choose a reason for hiding this comment

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

Those maps are private and there are not public getter methods for them in the DBErrorDetailsProvider. Are you saying that we can modify the class and raise a PR in thehydrator-plugins repo?

I specifically pointed out the method which can be called in earlier comment : https://github.com/cdapio/hydrator-plugins/blob/b9723d1b24068347c60ad2b0489064902f335de9/hydrator-common/src/main/java/io/cdap/plugin/common/db/DBErrorDetailsProvider.java#L184

private void executeWithRetry(FailureCollector failureCollector, SettableArguments settableArguments,
Properties connectionProperties) throws SQLException {
try (Connection connection = RetryUtils.createConnectionWithRetry((RetryPolicy<Connection>) retryPolicy,
config.getConnectionString(), connectionProperties, null)) {
Copy link
Member

Choose a reason for hiding this comment

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

why is externalDocumentationLink passed as null, this is not correct.

You can create a method at top level which returns generic link and others can override it.

See how we do it in OracleErrorDetailsProvider, MysqlErrorDetailsProvider, etc.

Copy link
Member

@itsankit-google itsankit-google Jun 5, 2025

Choose a reason for hiding this comment

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

We can use this link as default one "https://en.wikipedia.org/wiki/SQLSTATE" as it is being used by DBErrorDetailsProvider. similar comment for AbstractDBSource & AbstractDBSink.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the default value for external documentation link in the above mentioned classes. 35537d7

Comment on lines +261 to +262
String errorMessageWithDetails = String.format("Error occurred while trying to" +
" get schema from database." + "Error message: '%s'. Error code: '%s'. SQLState: '%s'", e.getMessage(),
Copy link
Member

Choose a reason for hiding this comment

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

We are not always fetching schema from database in above calls.

Comment on lines +277 to +308
/**
* Get the {@link ErrorCategory} for the given SQL state.
* Implements generic error categories based on the SQL state.
* See <a href="https://en.wikipedia.org/wiki/SQLSTATE">SQLSTATE</a> for more information.
* Override this method to provide custom error categories based on the SQL state.
*
* @param sqlState The SQL state.
* @return The {@link ErrorCategory} for the given SQL state.
*/
private static ErrorCategory getErrorCategoryFromSqlState(String sqlState) {
if (!Strings.isNullOrEmpty(sqlState) && sqlState.length() >= 2 &&
ERROR_CODE_TO_ERROR_CATEGORY.containsKey(sqlState.substring(0, 2))) {
return ERROR_CODE_TO_ERROR_CATEGORY.get(sqlState.substring(0, 2));
}
return new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN);
}

/**
* Get the {@link ErrorType} for the given error code and SQL state.
* Override this method to provide custom error types based on the error code and SQL state.
*
* @param errorCode The error code.
* @param sqlState The SQL state.
* @return The {@link ErrorType} for the given error code and SQL state.
*/
private static ErrorType getErrorTypeFromErrorCodeAndSqlState(int errorCode, String sqlState) {
if (!Strings.isNullOrEmpty(sqlState) && sqlState.length() >= 2 &&
ERROR_CODE_TO_ERROR_TYPE.containsKey(sqlState.substring(0, 2))) {
return ERROR_CODE_TO_ERROR_TYPE.get(sqlState.substring(0, 2));
}
return ErrorType.UNKNOWN;
}
Copy link
Member

Choose a reason for hiding this comment

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

As I said before this is not needed, we could just create an object of DBErrorDetailsProvider and use DBErrorDetailsProvider#getProgramFailureException to do all of this stuff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants