-
Notifications
You must be signed in to change notification settings - Fork 34
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
base: develop
Are you sure you want to change the base?
Conversation
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. |
7fcb0a0
to
9da75b8
Compare
9da75b8
to
ac813f0
Compare
There was a problem hiding this 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.
database-commons/src/main/java/io/cdap/plugin/db/action/DBRun.java
Outdated
Show resolved
Hide resolved
database-commons/src/main/java/io/cdap/plugin/db/source/AbstractDBSource.java
Outdated
Show resolved
Hide resolved
database-commons/src/main/java/io/cdap/plugin/db/source/AbstractDBSource.java
Outdated
Show resolved
Hide resolved
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. |
database-commons/src/main/java/io/cdap/plugin/db/sink/AbstractDBSink.java
Outdated
Show resolved
Hide resolved
database-commons/src/main/java/io/cdap/plugin/db/sink/AbstractDBSink.java
Outdated
Show resolved
Hide resolved
database-commons/src/main/java/io/cdap/plugin/db/source/AbstractDBSource.java
Outdated
Show resolved
Hide resolved
database-commons/src/main/java/io/cdap/plugin/db/source/AbstractDBSource.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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?
database-commons/src/main/java/io/cdap/plugin/db/ConnectionConfig.java
Outdated
Show resolved
Hide resolved
cloudsql-mysql-plugin/src/test/java/io/cdap/plugin/cloudsql/mysql/CloudSQLMySQLSinkTest.java
Show resolved
Hide resolved
database-commons/src/main/java/io/cdap/plugin/db/connector/AbstractDBSpecificConnector.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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.
database-commons/src/main/java/io/cdap/plugin/db/source/AbstractDBSource.java
Outdated
Show resolved
Hide resolved
Refactored the code to add the retry logic only for the methods interacting with the JDBC client. |
database-commons/src/main/java/io/cdap/plugin/db/source/AbstractDBSource.java
Outdated
Show resolved
Hide resolved
database-commons/src/main/java/io/cdap/plugin/util/RetryUtils.java
Outdated
Show resolved
Hide resolved
database-commons/src/main/java/io/cdap/plugin/db/connector/AbstractDBConnectorConfig.java
Outdated
Show resolved
Hide resolved
database-commons/src/main/java/io/cdap/plugin/db/sink/AbstractDBSink.java
Outdated
Show resolved
Hide resolved
database-commons/src/main/java/io/cdap/plugin/db/source/AbstractDBSource.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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?
Yes. Will check and update the behaviour here. I've reverted the changes done to handle 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: [AFTER CHANGES] Error message on the UI: Is the new error message acceptable or should we revert the changes done to throw |
LGTM |
database-commons/src/main/java/io/cdap/plugin/db/source/AbstractDBSource.java
Outdated
Show resolved
Hide resolved
database-commons/src/main/java/io/cdap/plugin/db/sink/AbstractDBSink.java
Outdated
Show resolved
Hide resolved
protected String getExternalDocumentationLink() { | ||
return null; | ||
return "https://en.wikipedia.org/wiki/SQLSTATE"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this method still needed? can we remove it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not needed now. Removed. 7c8815d
protected String getExternalDocumentationLink() { | ||
return "https://en.wikipedia.org/wiki/SQLSTATE"; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this method still needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, not needed now. Removed it. 7c8815d
@@ -87,13 +86,17 @@ public abstract class AbstractDBSource<T extends PluginConfig & DatabaseSourceCo | |||
Pattern.CASE_INSENSITIVE); | |||
private static final Pattern WHERE_CONDITIONS = Pattern.compile("\\s+where \\$conditions", | |||
Pattern.CASE_INSENSITIVE); | |||
private final RetryPolicy<?> retryPolicy; | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please remove empty line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed 0b57ce6
c6de31b
to
26efc64
Compare
0e9b88e
to
2284f2d
Compare
@@ -1,9 +1,9 @@ | |||
errorMessageInvalidSourceDatabase=SQL error while getting query schema: Error: Unknown database 'invalidDatabase', SQLState: 42000, ErrorCode: 1049 | |||
errorMessageInvalidSourceDatabase=errorMessage: SQL Exception occurred |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still see generic error messages which is bad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The earlier error message is changed as per the new code, and hence as per the latest code updated the error message not include the dynamic/privacy data present in error msgs here it contains.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As suggested, updated the error message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we please add Open and Capture Logs
step to these scenarios :
-
database-plugins/mssql-plugin/src/e2e-test/features/mssql/mssql source/RunTimeWithMacros.feature
Line 217 in e5c5794
Scenario: Verify pipeline failure message in logs when user provides invalid Table name in importQuery of plugin with Macros -
database-plugins/mssql-plugin/src/e2e-test/features/mssql/mssql source/RunTimeWithMacros.feature
Line 258 in e5c5794
Scenario: Verify pipeline failure message in logs when user provides invalid Credentials for connection with Macros -
database-plugins/cloudsql-postgresql-plugin/src/e2e-test/features/source/RunTime.feature
Line 114 in e5c5794
Scenario: To verify pipeline failure message in logs when an invalid bounding query is provided -
database-plugins/cloudsql-postgresql-plugin/src/e2e-test/features/source/RunTime.feature
Line 156 in e5c5794
Scenario: To verify the pipeline fails while preview with invalid bounding query setting the split-By field
} else if (cause instanceof RuntimeException) { | ||
return (RuntimeException) cause; | ||
} else if (cause instanceof Error) { | ||
return new RuntimeException("Failsafe wrapped an Error", cause); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return new RuntimeException("Operation failed with error", cause);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated 7977d45
} else if (cause instanceof Error) { | ||
return new RuntimeException("Failsafe wrapped an Error", cause); | ||
} else { | ||
return new RuntimeException("Failsafe wrapped a non-runtime exception", cause); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return new RuntimeException("Operation failed", cause);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated 7977d45
Can we address these changes in other PR? We will create a new ticket to address these changes. Please confirm. |
I am not sure how to verify the e2e changes then? |
cbab045
to
1c0bfe5
Compare
Changes done. |
7977d45
to
40ff60a
Compare
database-commons/src/main/java/io/cdap/plugin/util/RetryUtils.java
Outdated
Show resolved
Hide resolved
7e16744
to
3f332d1
Compare
hi @itsankit-google, Please let me know if there are any new review comments to be addressed. If not, can we approve it? |
148f5a7
to
766132b
Compare
766132b
to
40264b9
Compare
connection, String.format("SELECT %s FROM %s WHERE 1 = 0", dbColumns, fullyQualifiedTableName), | ||
getErrorDetailsProvider()); | ||
ResultSet rs = RetryUtils.executeQueryWithRetry((RetryPolicy<ResultSet>) retryPolicy, pStmt, | ||
getErrorDetailsProvider())) { | ||
getFieldsValidator().validateFields(inputSchema, rs, collector); | ||
} | ||
} catch (SQLException e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this catch is needed now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's needed to handle the SQLException
thrown from the auto-closeable resource used in the try with resources block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both the places where ever we are connecting to SQL driver, I can see RetryUtils getting used, when will it go in this cache clause?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might go.
As we're using try-with-resources, this will auto close the resources being used by implicitly calling close()
method on each resource in the reverse order of declaration.
Since close()
can throw SQLException
, the compiler enforces that we either:
- catch it, or
- declare it in the method signature
Furthermore, we also have methods such as getMetaData()
, getTables()
and next()
that can possibly throw SQLException
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add sqlState and errorCode in the error message of catch block as well?
inferredFields.addAll(getSchemaReader().getSchemaFields(rs)); | ||
} | ||
} catch (SQLException e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should also add it here then?
Otherwise it will fallback to lower one which will show incorrect error message
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)