fix: fix MySQL stale connection error (#24429)

## Description
- Add changes to address the`StaleConnection` exception caused by MySQL plugin.
- Update MySQL driver version.
- Other refactor changes not related to the main issue: 
  - Explicit empty constructor definition is replaced with Lombok annotation for all error messages class.
  - A base class is created for plugin error messages class to store all common error messages.
  - Fix Indentation.
This commit is contained in:
Sumit Kumar 2023-06-30 11:40:05 +05:30 committed by GitHub
parent 4ce12be224
commit 98a509227f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 2162 additions and 1697 deletions

View File

@ -11,4 +11,24 @@ public interface PluginConstants {
String AMAZON_S3_PLUGIN = "amazons3-plugin";
String GOOGLE_SHEETS_PLUGIN = "google-sheets-plugin";
}
interface PluginName {
public static final String S3_PLUGIN_NAME = "S3";
public static final String ARANGO_PLUGIN_NAME = "Arango";
public static final String DYNAMO_PLUGIN_NAME = "Dynamo";
public static final String ELASTIC_SEARCH_PLUGIN_NAME = "ElasticSearch";
public static final String FIRESTORE_PLUGIN_NAME = "Firestore";
public static final String GOOGLE_SHEETS_PLUGIN_NAME = "GoogleSheets";
public static final String GRAPHQL_PLUGIN_NAME = "Graphql";
public static final String MSSQL_PLUGIN_NAME = "Mssql";
public static final String MYSQL_PLUGIN_NAME = "Mysql";
public static final String ORACLE_PLUGIN_NAME = "Oracle";
public static final String POSTGRES_PLUGIN_NAME = "Postgres";
public static final String REDIS_PLUGIN_NAME = "Redis";
public static final String REDSHIFT_PLUGIN_NAME = "Redshift";
public static final String REST_API_PLUGIN_NAME = "RestApi";
public static final String SAAS_PLUGIN_NAME = "Saas";
public static final String SMTP_PLUGIN_NAME = "Smtp";
public static final String SNOWFLAKE_PLUGIN_NAME = "Snowflake";
}
}

View File

@ -0,0 +1,13 @@
package com.appsmith.external.exceptions.pluginExceptions;
public abstract class BasePluginErrorMessages {
public static final String CONNECTION_INVALID_ERROR_MSG = "Connection object is invalid.";
public static final String CONNECTION_NULL_ERROR_MSG = "Connection object is null.";
public static final String CONNECTION_CLOSED_ERROR_MSG = "Connection object is closed.";
public static final String CONNECTION_POOL_NULL_ERROR_MSG = "Connection pool is null.";
public static final String CONNECTION_POOL_CLOSED_ERROR_MSG = "Connection pool is closed.";
public static final String CONNECTION_POOL_NOT_RUNNING_ERROR_MSG = "Connection pool is not running.";
public static final String UNKNOWN_CONNECTION_ERROR_MSG = "Unknown connection error. Please reach out to Appsmith " +
"customer support to resolve this.";
public static final String JDBC_DRIVER_LOADING_ERROR_MSG = "Error loading JDBC Driver class.";
}

View File

@ -1,14 +1,22 @@
package com.appsmith.external.exceptions.pluginExceptions;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class StaleConnectionException extends RuntimeException {
public StaleConnectionException() {
}
String message = "";
public StaleConnectionException(String message) {
super(message);
this.message = message;
}
public StaleConnectionException(String message, Throwable cause) {
super(message, cause);
this.message = message;
}
}

View File

@ -5,9 +5,7 @@ import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.models.Condition;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.io.IOException;

View File

@ -71,10 +71,10 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.LinkedHashMap;
import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY;
import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_PATH;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_NULL_ERROR_MSG;
import static com.appsmith.external.helpers.PluginUtils.OBJECT_TYPE;
import static com.appsmith.external.helpers.PluginUtils.STRING_TYPE;
import static com.appsmith.external.helpers.PluginUtils.getDataValueSafelyFromFormData;
@ -459,7 +459,7 @@ public class AmazonS3Plugin extends BasePlugin {
* - If connection object is null, then assume stale connection.
*/
if (connection == null) {
return Mono.error(new StaleConnectionException());
return Mono.error(new StaleConnectionException(CONNECTION_NULL_ERROR_MSG));
}
if (actionConfiguration == null) {
@ -787,7 +787,7 @@ public class AmazonS3Plugin extends BasePlugin {
}
return Mono.just(actionResult);
})
.onErrorMap(IllegalStateException.class, error -> new StaleConnectionException())
.onErrorMap(IllegalStateException.class, error -> new StaleConnectionException(error.getMessage()))
.flatMap(obj -> obj)
.flatMap(result -> {
ActionExecutionResult actionExecutionResult = new ActionExecutionResult();
@ -986,8 +986,8 @@ public class AmazonS3Plugin extends BasePlugin {
S3ErrorMessages.LIST_OF_BUCKET_FETCHING_ERROR_MSG,
e.getMessage()
);
} catch (IllegalStateException s) {
throw new StaleConnectionException();
} catch (IllegalStateException e) {
throw new StaleConnectionException(e.getMessage());
}
return new DatasourceStructure(tableList);

View File

@ -1,9 +1,11 @@
package com.external.plugins.exceptions;
public class S3ErrorMessages {
private S3ErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class S3ErrorMessages extends BasePluginErrorMessages {
public static final String FILE_CONTENT_FETCHING_ERROR_MSG = "Appsmith server has encountered an unexpected error when fetching file " +
"content from AWS S3 server. Please reach out to Appsmith customer support to resolve this.";
@ -99,6 +101,4 @@ public class S3ErrorMessages {
public static final String DS_MANDATORY_PARAMETER_ENDPOINT_URL_MISSING_ERROR_MSG = "Required parameter 'Endpoint URL' is empty. Did you forget to edit the 'Endpoint" +
" URL' field in the datasource creation form ? You need to fill it with " +
"the endpoint URL of your S3 instance.";
}

View File

@ -26,7 +26,6 @@ import com.arangodb.model.CollectionsReadOptions;
import com.external.plugins.exceptions.ArangoDBErrorMessages;
import com.external.plugins.exceptions.ArangoDBPluginError;
import com.external.utils.ArangoDBErrorUtils;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ObjectUtils;
import org.pf4j.Extension;
@ -51,6 +50,7 @@ import java.util.stream.Collectors;
import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY;
import static com.appsmith.external.helpers.PluginUtils.MATCH_QUOTED_WORDS_REGEX;
import static com.external.plugins.exceptions.ArangoDBErrorMessages.CONNECTION_INVALID_ERROR_MSG;
import static com.external.plugins.exceptions.ArangoDBErrorMessages.DS_HOSTNAME_MISSING_OR_INVALID_ERROR_MSG;
import static com.external.utils.SSLUtils.isCaCertificateAvailable;
import static com.external.utils.SSLUtils.setSSLContext;
@ -84,7 +84,7 @@ public class ArangoDBPlugin extends BasePlugin {
ActionConfiguration actionConfiguration) {
if (!isConnectionValid(db)) {
return Mono.error(new StaleConnectionException());
return Mono.error(new StaleConnectionException(CONNECTION_INVALID_ERROR_MSG));
}
String query = actionConfiguration.getBody();

View File

@ -1,9 +1,11 @@
package com.external.plugins.exceptions;
public class ArangoDBErrorMessages {
private ArangoDBErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class ArangoDBErrorMessages extends BasePluginErrorMessages {
public static final String MISSING_QUERY_ERROR_MSG = "Missing required parameter: Query.";
public static final String QUERY_EXECUTION_FAILED_ERROR_MSG = "Your query failed to execute. Please check more information in the error details.";
@ -20,7 +22,6 @@ public class ArangoDBErrorMessages {
public static final String GET_STRUCTURE_ERROR_MSG = "Appsmith server has failed to fetch list of collections from database. Please check " +
"if the database credentials are valid and/or you have the required permissions.";
/*
************************************************************************************************************************************************
Error messages related to validation of datasource.

View File

@ -1,10 +1,11 @@
package com.external.plugins.exceptions;
public class DynamoErrorMessages {
private DynamoErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class DynamoErrorMessages extends BasePluginErrorMessages {
public static final String MISSING_ACTION_NAME_ERROR_MSG = "Missing action name (like `ListTables`, `GetItem` etc.).";
public static final String UNKNOWN_ACTION_NAME_ERROR_MSG = "Unknown action: `%s`. Note that action names are case-sensitive.";

View File

@ -1,10 +1,11 @@
package com.external.plugins.exceptions;
public class ElasticSearchErrorMessages {
private ElasticSearchErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class ElasticSearchErrorMessages extends BasePluginErrorMessages {
public static final String ARRAY_TO_ND_JSON_ARRAY_CONVERSION_ERROR_MSG = "Error occurred while converting array to ND-JSON";
public static final String QUERY_EXECUTION_FAILED_ERROR_MSG = "Error occurred while executing Elasticsearch query.";

View File

@ -1,10 +1,11 @@
package com.external.plugins.exceptions;
public class FirestoreErrorMessages {
private FirestoreErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class FirestoreErrorMessages extends BasePluginErrorMessages {
public static final String MANDATORY_PARAM_COMMAND_MISSING_ERROR_MSG = "Mandatory parameter 'Command' is missing. Did you forget to select one of the commands" +
" from the Command dropdown ?";

View File

@ -1,6 +1,11 @@
package com.external.constants;
public class ErrorMessages {
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class ErrorMessages extends BasePluginErrorMessages {
public static final String EMPTY_ROW_OBJECT_MESSAGE = "Row object(s) cannot be empty.";

View File

@ -1,9 +1,11 @@
package com.external.plugins.exceptions;
public class GraphQLErrorMessages {
private GraphQLErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class GraphQLErrorMessages extends BasePluginErrorMessages {
public static final String URI_SYNTAX_WRONG_ERROR_MSG = "URI is invalid. Please rectify the URI and try again.";
public static final String INVALID_CONTENT_TYPE_ERROR_MSG = "Invalid value for Content-Type.";
public static final String NO_HTTP_METHOD_ERROR_MSG = "HTTPMethod must be set.";

View File

@ -116,6 +116,7 @@ import static com.external.plugins.constants.FieldName.SMART_SUBSTITUTION;
import static com.external.plugins.constants.FieldName.SUCCESS;
import static com.external.plugins.constants.FieldName.UPDATE_OPERATION;
import static com.external.plugins.constants.FieldName.UPDATE_QUERY;
import static com.external.plugins.exceptions.MongoPluginErrorMessages.MONGO_CLIENT_NULL_ERROR_MSG;
import static com.external.plugins.utils.DatasourceUtils.KEY_PASSWORD;
import static com.external.plugins.utils.DatasourceUtils.KEY_URI_DEFAULT_DBNAME;
import static com.external.plugins.utils.DatasourceUtils.KEY_USERNAME;
@ -134,7 +135,6 @@ import static com.external.plugins.utils.MongoPluginUtils.isRawCommand;
import static java.lang.Boolean.TRUE;
import static java.util.Arrays.asList;
import static org.apache.logging.log4j.util.Strings.isBlank;
import static org.apache.logging.log4j.util.Strings.isEmpty;
public class MongoPlugin extends BasePlugin {
@ -310,7 +310,7 @@ public class MongoPlugin extends BasePlugin {
if (mongoClient == null) {
log.info("Encountered null connection in MongoDB plugin. Reporting back.");
throw new StaleConnectionException();
throw new StaleConnectionException(MONGO_CLIENT_NULL_ERROR_MSG);
}
Mono<Document> mongoOutputMono;
ActionExecutionResult result = new ActionExecutionResult();
@ -354,13 +354,13 @@ public class MongoPlugin extends BasePlugin {
*/
.onErrorMap(
IllegalStateException.class,
error -> new StaleConnectionException()
error -> new StaleConnectionException(error.getMessage())
)
// This is an experimental fix to handle the scenario where after a period of inactivity, the mongo
// database drops the connection which makes the client throw the following exception.
.onErrorMap(
MongoSocketWriteException.class,
error -> new StaleConnectionException()
error -> new StaleConnectionException(error.getMessage())
)
.flatMap(mongoOutput -> {
try {
@ -917,13 +917,13 @@ public class MongoPlugin extends BasePlugin {
*/
.onErrorMap(
IllegalStateException.class,
error -> new StaleConnectionException()
error -> new StaleConnectionException(error.getMessage())
)
// This is an experimental fix to handle the scenario where after a period of inactivity, the mongo
// database drops the connection which makes the client throw the following exception.
.onErrorMap(
MongoSocketWriteException.class,
error -> new StaleConnectionException()
error -> new StaleConnectionException(error.getMessage())
)
.onErrorMap(
MongoCommandException.class,

View File

@ -33,6 +33,8 @@ public class MongoPluginErrorMessages {
public static final String QUERY_INVALID_ERROR_MSG = "Your query is invalid";
public static final String MONGO_CLIENT_NULL_ERROR_MSG = "Mongo client object is null.";
/*
************************************************************************************************************************************************
Error messages related to validation of datasource.

View File

@ -27,7 +27,6 @@
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>

View File

@ -66,10 +66,13 @@ import java.util.Set;
import java.util.stream.IntStream;
import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY;
import static com.appsmith.external.constants.PluginConstants.PluginName.MSSQL_PLUGIN_NAME;
import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns;
import static com.appsmith.external.helpers.PluginUtils.getPSParamLabel;
import static com.appsmith.external.helpers.SmartSubstitutionHelper.replaceQuestionMarkWithDollarIndex;
import static com.external.plugins.utils.MssqlDatasourceUtils.getConnectionFromConnectionPool;
import static com.external.plugins.exceptions.MssqlErrorMessages.CONNECTION_CLOSED_ERROR_MSG;
import static com.external.plugins.exceptions.MssqlErrorMessages.CONNECTION_INVALID_ERROR_MSG;
import static com.external.plugins.exceptions.MssqlErrorMessages.CONNECTION_NULL_ERROR_MSG;
import static com.external.plugins.utils.MssqlDatasourceUtils.logHikariCPStatus;
import static com.external.plugins.utils.MssqlExecuteUtils.closeConnectionPostExecution;
import static java.lang.Boolean.FALSE;
@ -90,6 +93,8 @@ public class MssqlPlugin extends BasePlugin {
private static final long MS_SQL_DEFAULT_PORT = 1433L;
public static final MssqlDatasourceUtils mssqlDatasourceUtils = new MssqlDatasourceUtils();
public MssqlPlugin(PluginWrapper wrapper) {
super(wrapper);
}
@ -190,19 +195,36 @@ public class MssqlPlugin extends BasePlugin {
final List<String> columnsList = new ArrayList<>();
try {
sqlConnectionFromPool = getConnectionFromConnectionPool(hikariDSConnection);
sqlConnectionFromPool =
mssqlDatasourceUtils.getConnectionFromHikariConnectionPool(hikariDSConnection,
MSSQL_PLUGIN_NAME);
} catch (SQLException | StaleConnectionException e) {
// The function can throw either StaleConnectionException or SQLException. The underlying hikari
// library throws SQLException in case the pool is closed or there is an issue initializing
// the connection pool which can also be translated in our world to StaleConnectionException
// and should then trigger the destruction and recreation of the pool.
return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException());
return Mono.error(e instanceof StaleConnectionException ? e :
new StaleConnectionException(e.getMessage()));
}
try {
if (sqlConnectionFromPool == null || sqlConnectionFromPool.isClosed() || !sqlConnectionFromPool.isValid(VALIDITY_CHECK_TIMEOUT)) {
log.info("Encountered stale connection in MsSQL plugin. Reporting back.");
return Mono.error(new StaleConnectionException());
if (sqlConnectionFromPool == null) {
return Mono.error(new StaleConnectionException(CONNECTION_NULL_ERROR_MSG));
}
else if (sqlConnectionFromPool.isClosed()) {
return Mono.error(new StaleConnectionException(CONNECTION_CLOSED_ERROR_MSG));
}
else {
/**
* Not adding explicit `!sqlConnectionFromPool.isValid(VALIDITY_CHECK_TIMEOUT)`
* check here because this check may take few seconds to complete hence adding
* extra time delay.
*/
return Mono.error(new StaleConnectionException(CONNECTION_INVALID_ERROR_MSG));
}
}
} catch (SQLException error) {
// This exception is thrown only when the timeout to `isValid` is negative. Since, that's not the case,

View File

@ -1,10 +1,11 @@
package com.external.plugins.exceptions;
public class MssqlErrorMessages {
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
private MssqlErrorMessages() {
//Prevents instantiation
}
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class MssqlErrorMessages extends BasePluginErrorMessages {
public static final String MISSING_QUERY_ERROR_MSG = "Missing required parameter: Query.";
public static final String QUERY_EXECUTION_FAILED_ERROR_MSG = "Your query failed to execute. Please check more information in the error details.";

View File

@ -8,8 +8,6 @@ import com.appsmith.external.models.DatasourceStructure;
import com.external.plugins.exceptions.MssqlErrorMessages;
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
@ -24,11 +22,16 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static com.appsmith.external.constants.PluginConstants.PluginName.MSSQL_PLUGIN_NAME;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_CLOSED_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NOT_RUNNING_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NULL_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.UNKNOWN_CONNECTION_ERROR_MSG;
import static com.appsmith.external.helpers.PluginUtils.safelyCloseSingleConnectionFromHikariCP;
import static com.external.plugins.MssqlPlugin.MssqlPluginExecutor.scheduler;
import static com.external.plugins.MssqlPlugin.mssqlDatasourceUtils;
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MssqlDatasourceUtils {
public static final String PRIMARY_KEY_INDICATOR = "PRIMARY KEY";
@ -94,13 +97,15 @@ public class MssqlDatasourceUtils {
return Mono.fromSupplier(() -> {
Connection connectionFromPool;
try {
connectionFromPool = getConnectionFromConnectionPool(connection);
connectionFromPool = mssqlDatasourceUtils.getConnectionFromHikariConnectionPool(connection,
MSSQL_PLUGIN_NAME);
} catch (SQLException | StaleConnectionException e) {
// The function can throw either StaleConnectionException or SQLException. The
// underlying hikari library throws SQLException in case the pool is closed or there is an issue
// initializing the connection pool which can also be translated in our world to
// StaleConnectionException and should then trigger the destruction and recreation of the pool.
return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException());
return Mono.error(e instanceof StaleConnectionException ? e :
new StaleConnectionException(e.getMessage()));
}
logHikariCPStatus("Before getting Mssql DB schema", connection);
@ -137,21 +142,9 @@ public class MssqlDatasourceUtils {
* First checks if the connection pool is still valid. If yes, we fetch a connection from the pool and return
* In case a connection is not available in the pool, SQL Exception is thrown
*
* @param hikariDSConnectionPool
* @param connectionPool
* @return SQL Connection
*/
public static Connection getConnectionFromConnectionPool(HikariDataSource hikariDSConnectionPool) throws SQLException {
if (hikariDSConnectionPool == null || hikariDSConnectionPool.isClosed() || !hikariDSConnectionPool.isRunning()) {
log.debug("Encountered stale connection pool in SQL Server plugin. Reporting back.");
throw new StaleConnectionException();
}
Connection sqlDataSourceConnection = hikariDSConnectionPool.getConnection();
return sqlDataSourceConnection;
}
public static void logHikariCPStatus(String logPrefix, HikariDataSource connectionPool) {
HikariPoolMXBean poolProxy = connectionPool.getHikariPoolMXBean();
int idleConnections = poolProxy.getIdleConnections();
@ -305,4 +298,36 @@ public class MssqlDatasourceUtils {
return MessageFormat.format("{0}={1}", columnNameToSampleColumnDataMap.keySet().stream().findFirst().orElse(
"id"), columnNameToSampleColumnDataMap.values().stream().findFirst().orElse("'uid'"));
}
public void checkHikariCPConnectionPoolValidity(HikariDataSource connectionPool, String pluginName) throws StaleConnectionException {
if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) {
String printMessage = MessageFormat.format(Thread.currentThread().getName() +
": Encountered stale connection pool in {0} plugin. Reporting back.", pluginName);
System.out.println(printMessage);
if (connectionPool == null) {
throw new StaleConnectionException(CONNECTION_POOL_NULL_ERROR_MSG);
}
else if (connectionPool.isClosed()) {
throw new StaleConnectionException(CONNECTION_POOL_CLOSED_ERROR_MSG);
}
else if (!connectionPool.isRunning()) {
throw new StaleConnectionException(CONNECTION_POOL_NOT_RUNNING_ERROR_MSG);
}
else {
/**
* Ideally, code flow is never expected to reach here. However, this section has been added to catch
* those cases wherein a developer updates the parent if condition but does not update the nested
* if else conditions.
*/
throw new StaleConnectionException(UNKNOWN_CONNECTION_ERROR_MSG);
}
}
}
public Connection getConnectionFromHikariConnectionPool(HikariDataSource connectionPool,
String pluginName) throws SQLException {
checkHikariCPConnectionPoolValidity(connectionPool, pluginName);
return connectionPool.getConnection();
}
}

View File

@ -4,6 +4,7 @@ import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Endpoint;
import com.appsmith.external.models.SSLDetails;
import com.external.plugins.utils.MssqlDatasourceUtils;
import com.zaxxer.hikari.HikariDataSource;
import org.testcontainers.containers.MSSQLServerContainer;
import org.testcontainers.utility.DockerImageName;
@ -12,13 +13,15 @@ import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import static com.external.plugins.utils.MssqlDatasourceUtils.getConnectionFromConnectionPool;
import static com.appsmith.external.constants.PluginConstants.PluginName.MSSQL_PLUGIN_NAME;
import static com.external.plugins.utils.MssqlExecuteUtils.closeConnectionPostExecution;
public class MssqlTestDBContainerManager {
static MssqlPlugin.MssqlPluginExecutor mssqlPluginExecutor = new MssqlPlugin.MssqlPluginExecutor();
public static MssqlDatasourceUtils mssqlDatasourceUtils = new MssqlDatasourceUtils();
@SuppressWarnings("rawtypes")
public static MSSQLServerContainer getMssqlDBForTest() {
return new MSSQLServerContainer<>(
@ -56,7 +59,8 @@ public class MssqlTestDBContainerManager {
}
static void runSQLQueryOnMssqlTestDB(String sqlQuery, HikariDataSource sharedConnectionPool) throws SQLException {
java.sql.Connection connectionFromPool = getConnectionFromConnectionPool(sharedConnectionPool);
java.sql.Connection connectionFromPool =
mssqlDatasourceUtils.getConnectionFromHikariConnectionPool(sharedConnectionPool, MSSQL_PLUGIN_NAME);
Statement statement = connectionFromPool.createStatement();
statement.execute(sqlQuery);
closeConnectionPostExecution(null, statement, null, connectionFromPool);

View File

@ -19,7 +19,7 @@
<dependency>
<groupId>org.mariadb</groupId>
<artifactId>r2dbc-mariadb</artifactId>
<version>1.1.3</version>
<version>1.1.4</version>
<exclusions>
<exclusion>
<groupId>io.netty</groupId>

View File

@ -66,6 +66,7 @@ import static com.appsmith.external.helpers.PluginUtils.MATCH_QUOTED_WORDS_REGEX
import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns;
import static com.appsmith.external.helpers.PluginUtils.getPSParamLabel;
import static com.appsmith.external.helpers.SmartSubstitutionHelper.replaceQuestionMarkWithDollarIndex;
import static com.external.plugins.exceptions.MySQLErrorMessages.CONNECTION_VALIDITY_CHECK_FAILED_ERROR_MSG;
import static com.external.utils.MySqlDatasourceUtils.getNewConnectionPool;
import static com.external.utils.MySqlGetStructureUtils.getKeyInfo;
import static com.external.utils.MySqlGetStructureUtils.getTableInfo;
@ -256,9 +257,9 @@ public class MySqlPlugin extends BasePlugin {
connectionPool.create(),
connection -> {
// TODO: add JUnit TC for the `connection.validate` check. Not sure how to do it at the moment.
Flux<Result> resultFlux = Mono.from(connection.validate(ValidationDepth.REMOTE))
Flux<Result> resultFlux = Mono.from(connection.validate(ValidationDepth.LOCAL))
.timeout(Duration.ofSeconds(VALIDATION_CHECK_TIMEOUT))
.onErrorMap(TimeoutException.class, error -> new StaleConnectionException())
.onErrorMap(TimeoutException.class, error -> new StaleConnectionException(error.getMessage()))
.flatMapMany(isValid -> {
if (isValid) {
return createAndExecuteQueryFromConnection(finalQuery,
@ -269,7 +270,7 @@ public class MySqlPlugin extends BasePlugin {
requestData,
psParams);
}
return Flux.error(new StaleConnectionException());
return Flux.error(new StaleConnectionException(CONNECTION_VALIDITY_CHECK_FAILED_ERROR_MSG));
});
Mono<List<Map<String, Object>>> resultMono;
@ -348,9 +349,10 @@ public class MySqlPlugin extends BasePlugin {
},
Connection::close
)
.onErrorMap(TimeoutException.class, error -> new StaleConnectionException())
.onErrorMap(PoolShutdownException.class, error -> new StaleConnectionException())
.onErrorMap(R2dbcNonTransientResourceException.class, error -> new StaleConnectionException())
.onErrorMap(TimeoutException.class, error -> new StaleConnectionException(error.getMessage()))
.onErrorMap(PoolShutdownException.class, error -> new StaleConnectionException(error.getMessage()))
.onErrorMap(R2dbcNonTransientResourceException.class, error -> new StaleConnectionException(error.getMessage()))
.onErrorMap(IllegalStateException.class, error -> new StaleConnectionException(error.getMessage()))
.subscribeOn(scheduler);
}
@ -579,60 +581,58 @@ public class MySqlPlugin extends BasePlugin {
return Mono.usingWhen(
connectionPool.create(),
connection -> {
return Mono.from(connection.validate(ValidationDepth.REMOTE))
.timeout(Duration.ofSeconds(VALIDATION_CHECK_TIMEOUT))
.onErrorMap(TimeoutException.class, error -> new StaleConnectionException())
.flatMapMany(isValid -> {
if (isValid) {
return connection.createStatement(COLUMNS_QUERY).execute();
} else {
return Flux.error(new StaleConnectionException());
}
})
.flatMap(result -> {
return result.map((row, meta) -> {
getTableInfo(row, meta, tablesByName);
connection -> Mono.from(connection.validate(ValidationDepth.REMOTE))
.timeout(Duration.ofSeconds(VALIDATION_CHECK_TIMEOUT))
.onErrorMap(TimeoutException.class, error -> new StaleConnectionException(error.getMessage()))
.flatMapMany(isValid -> {
if (isValid) {
return connection.createStatement(COLUMNS_QUERY).execute();
} else {
return Flux.error(new StaleConnectionException(CONNECTION_VALIDITY_CHECK_FAILED_ERROR_MSG));
}
})
.flatMap(result -> {
return result.map((row, meta) -> {
getTableInfo(row, meta, tablesByName);
return result;
});
})
.collectList()
.thenMany(Flux.from(connection.createStatement(KEYS_QUERY).execute()))
.flatMap(result -> {
return result.map((row, meta) -> {
getKeyInfo(row, meta, tablesByName, keyRegistry);
return result;
});
})
.collectList()
.map(list -> {
/* Get templates for each table and put those in. */
getTemplates(tablesByName);
structure.setTables(new ArrayList<>(tablesByName.values()));
for (DatasourceStructure.Table table : structure.getTables()) {
table.getKeys().sort(Comparator.naturalOrder());
}
return structure;
})
.onErrorMap(e -> {
if (!(e instanceof AppsmithPluginException) && !(e instanceof StaleConnectionException)) {
return new AppsmithPluginException(
AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR,
MySQLErrorMessages.GET_STRUCTURE_ERROR_MSG,
e.getMessage()
);
}
return e;
return result;
});
},
})
.collectList()
.thenMany(Flux.from(connection.createStatement(KEYS_QUERY).execute()))
.flatMap(result -> {
return result.map((row, meta) -> {
getKeyInfo(row, meta, tablesByName, keyRegistry);
return result;
});
})
.collectList()
.map(list -> {
/* Get templates for each table and put those in. */
getTemplates(tablesByName);
structure.setTables(new ArrayList<>(tablesByName.values()));
for (DatasourceStructure.Table table : structure.getTables()) {
table.getKeys().sort(Comparator.naturalOrder());
}
return structure;
})
.onErrorMap(e -> {
if (!(e instanceof AppsmithPluginException) && !(e instanceof StaleConnectionException)) {
return new AppsmithPluginException(
AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR,
MySQLErrorMessages.GET_STRUCTURE_ERROR_MSG,
e.getMessage()
);
}
return e;
}),
Connection::close
)
.onErrorMap(TimeoutException.class, error -> new StaleConnectionException())
.onErrorMap(PoolShutdownException.class, error -> new StaleConnectionException())
.onErrorMap(TimeoutException.class, error -> new StaleConnectionException(error.getMessage()))
.onErrorMap(PoolShutdownException.class, error -> new StaleConnectionException(error.getMessage()))
.subscribeOn(scheduler);
}
}

View File

@ -1,9 +1,11 @@
package com.external.plugins.exceptions;
public class MySQLErrorMessages {
private MySQLErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class MySQLErrorMessages extends BasePluginErrorMessages {
public static final String MISSING_PARAMETER_QUERY_ERROR_MSG = "Missing required parameter: Query.";
public static final String IS_KEYWORD_NOT_SUPPORTED_IN_PS_ERROR_MSG = "Appsmith currently does not support the IS keyword with the prepared statement " +
@ -32,7 +34,6 @@ public class MySQLErrorMessages {
public static final String DS_MISSING_DATABASE_NAME_ERROR_MSG = "Missing database name.";
public static final String DS_SSL_CONFIGURATION_FETCHING_FAILED_ERROR_MSG = "Appsmith server has failed to fetch SSL configuration from datasource configuration form. " +
"Please reach out to Appsmith customer support to resolve this.";
public static final String CONNECTION_VALIDITY_CHECK_FAILED_ERROR_MSG = "Connection obtained from connection pool" +
" is invalid.";
}

View File

@ -8,7 +8,6 @@ import com.appsmith.external.models.Endpoint;
import com.appsmith.external.models.Property;
import com.appsmith.external.models.SSLDetails;
import com.external.plugins.exceptions.MySQLErrorMessages;
import com.external.plugins.exceptions.MySQLPluginError;
import io.r2dbc.pool.ConnectionPool;
import io.r2dbc.pool.ConnectionPoolConfiguration;
import io.r2dbc.spi.ConnectionFactoryOptions;
@ -25,14 +24,40 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static io.r2dbc.pool.PoolingConnectionFactoryProvider.MAX_SIZE;
import static io.r2dbc.spi.ConnectionFactoryOptions.SSL;
public class MySqlDatasourceUtils {
public static int MAX_CONNECTION_POOL_SIZE = 5;
private static final Duration MAX_IDLE_TIME = Duration.ofMinutes(10);
/**
* 1 sec is the recommended value as shown in the example here:
* https://mariadb.com/docs/xpand/connect/programming-languages/java-r2dbc/native/connection-pools/
*
* Current understanding is that the issue mentioned in #17324 is because of at least one of the connections
* malfunctioning and causing the reactor thread pool / scheduler to get stuck and not schedule new tasks.
* Setting max idle time value to 1 sec could also be seen as a precaution move to make sure that we don't land
* into a situation where an idle thread can malfunction.
*/
private static final Duration MAX_IDLE_TIME = Duration.ofSeconds(1);
/**
* Current understanding is that the issue mentioned in #17324 is because of at least one of the connections
* malfunctioning and causing the reactor thread pool / scheduler to get stuck and not schedule new tasks.
* Setting max lifetime value to 5 min is a precaution move to make sure that we don't land into a situation
* where an older connection can malfunction.
* To understand what this config means please check here: https://github.com/r2dbc/r2dbc-pool
*/
private static final Duration MAX_LIFE_TIME = Duration.ofMinutes(5);
/**
* Current understanding is that the issue mentioned in #17324 is because of at least one of the connections
* malfunctioning and causing the reactor thread pool / scheduler to get stuck and not schedule new tasks.
* Setting eviction time value to 5 min is a precaution move to make sure that we don't land into a situation
* where an older connection can malfunction.
* To understand what this config means please check here: https://github.com/r2dbc/r2dbc-pool
*/
public static final Duration BACKGROUND_EVICTION_TIME = Duration.ofMinutes(5);
public static ConnectionFactoryOptions.Builder getBuilder(DatasourceConfiguration datasourceConfiguration) {
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
@ -188,7 +213,10 @@ public class MySqlDatasourceUtils {
ConnectionPoolConfiguration configuration = ConnectionPoolConfiguration.builder(connectionFactory)
.maxIdleTime(MAX_IDLE_TIME)
.maxSize(MAX_CONNECTION_POOL_SIZE)
.backgroundEvictionInterval(BACKGROUND_EVICTION_TIME)
.maxLifeTime(MAX_LIFE_TIME)
.build();
return new ConnectionPool(configuration);
}
}

View File

@ -0,0 +1,117 @@
package com.external.plugins;
import com.appsmith.external.dtos.ExecuteActionDTO;
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.external.utils.MySqlDatasourceUtils;
import io.r2dbc.pool.ConnectionPool;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.R2dbcNonTransientResourceException;
import io.r2dbc.spi.ValidationDepth;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.pool.PoolShutdownException;
import reactor.test.StepVerifier;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeoutException;
import static com.external.plugins.exceptions.MySQLErrorMessages.CONNECTION_VALIDITY_CHECK_FAILED_ERROR_MSG;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MySqlStaleConnectionErrorMessageTest {
static MySqlPlugin.MySqlPluginExecutor pluginExecutor = new MySqlPlugin.MySqlPluginExecutor();
static MySqlDatasourceUtils mysqlDatasourceUtils = new MySqlDatasourceUtils();
@Test
public void testStaleConnectionExceptionReturnsUpstreamErrorOnTimeoutError() throws TimeoutException {
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setBody("select 1;");
ConnectionPool mockConnectionPool = mock(ConnectionPool.class);
String expectedErrorMessage = "Timeout exception from MockConnectionPool";
when(mockConnectionPool.create()).thenReturn(Mono.error(new TimeoutException(expectedErrorMessage)));
Mono<ActionExecutionResult> actionExecutionResultMono = pluginExecutor.executeCommon(mockConnectionPool, actionConfiguration, false, List.of(),
new ExecuteActionDTO(), new HashMap<>());
StepVerifier.create(actionExecutionResultMono)
.expectErrorSatisfies(error -> {
assertTrue(error instanceof StaleConnectionException);
assertEquals(expectedErrorMessage, error.getMessage());
})
.verify();
}
@Test
public void testStaleConnectionExceptionReturnsUpstreamErrorOnPoolShutdownError() throws TimeoutException {
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setBody("select 1;");
ConnectionPool mockConnectionPool = mock(ConnectionPool.class);
String expectedErrorMessage = "Timeout exception from MockConnectionPool";
when(mockConnectionPool.create()).thenReturn(Mono.error(new PoolShutdownException(expectedErrorMessage)));
Mono<ActionExecutionResult> actionExecutionResultMono = pluginExecutor.executeCommon(mockConnectionPool, actionConfiguration, false, List.of(),
new ExecuteActionDTO(), new HashMap<>());
StepVerifier.create(actionExecutionResultMono)
.expectErrorSatisfies(error -> {
assertTrue(error instanceof StaleConnectionException);
assertEquals(expectedErrorMessage, error.getMessage());
})
.verify();
}
@Test
public void testStaleConnectionExceptionReturnsUpstreamErrorOnIllegalStateError() throws TimeoutException {
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setBody("select 1;");
ConnectionPool mockConnectionPool = mock(ConnectionPool.class);
String expectedErrorMessage = "Timeout exception from MockConnectionPool";
when(mockConnectionPool.create()).thenReturn(Mono.error(new IllegalStateException(expectedErrorMessage)));
Mono<ActionExecutionResult> actionExecutionResultMono = pluginExecutor.executeCommon(mockConnectionPool, actionConfiguration, false, List.of(),
new ExecuteActionDTO(), new HashMap<>());
StepVerifier.create(actionExecutionResultMono)
.expectErrorSatisfies(error -> {
assertTrue(error instanceof StaleConnectionException);
assertEquals(expectedErrorMessage, error.getMessage());
})
.verify();
}
@Test
public void testStaleConnectionExceptionReturnsUpstreamErrorOnResourceError() throws TimeoutException {
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setBody("select 1;");
ConnectionPool mockConnectionPool = mock(ConnectionPool.class);
String expectedErrorMessage = "Timeout exception from MockConnectionPool";
when(mockConnectionPool.create()).thenReturn(Mono.error(new R2dbcNonTransientResourceException(expectedErrorMessage)));
Mono<ActionExecutionResult> actionExecutionResultMono = pluginExecutor.executeCommon(mockConnectionPool, actionConfiguration, false, List.of(),
new ExecuteActionDTO(), new HashMap<>());
StepVerifier.create(actionExecutionResultMono)
.expectErrorSatisfies(error -> {
assertTrue(error instanceof StaleConnectionException);
assertEquals(expectedErrorMessage, error.getMessage());
})
.verify();
}
@Test
public void testStaleConnectionExceptionReturnsUpstreamErrorOnInvalidConnection() throws TimeoutException {
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setBody("select 1;");
ConnectionPool mockConnectionPool = mock(ConnectionPool.class);
Connection mockConnection = mock(Connection.class);
when(mockConnectionPool.create()).thenReturn(Mono.just(mockConnection));
when(mockConnection.validate(ValidationDepth.LOCAL)).thenReturn(Mono.just(false));
when(mockConnection.close()).thenReturn(Mono.empty());
Mono<ActionExecutionResult> actionExecutionResultMono = pluginExecutor.executeCommon(mockConnectionPool, actionConfiguration, false, List.of(),
new ExecuteActionDTO(), new HashMap<>());
StepVerifier.create(actionExecutionResultMono)
.expectErrorSatisfies(error -> {
assertTrue(error instanceof StaleConnectionException);
assertEquals(CONNECTION_VALIDITY_CHECK_FAILED_ERROR_MSG, error.getMessage());
})
.verify();
}
}

View File

@ -58,6 +58,7 @@ import java.util.stream.IntStream;
import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY;
import static com.appsmith.external.constants.CommonFieldName.BODY;
import static com.appsmith.external.constants.CommonFieldName.PREPARED_STATEMENT;
import static com.appsmith.external.constants.PluginConstants.PluginName.ORACLE_PLUGIN_NAME;
import static com.appsmith.external.helpers.PluginUtils.OBJECT_TYPE;
import static com.appsmith.external.helpers.PluginUtils.STRING_TYPE;
import static com.appsmith.external.helpers.PluginUtils.getDataValueSafelyFromFormData;
@ -67,7 +68,6 @@ import static com.appsmith.external.helpers.PluginUtils.setDataValueSafelyInForm
import static com.appsmith.external.helpers.SmartSubstitutionHelper.replaceQuestionMarkWithDollarIndex;
import static com.external.plugins.utils.OracleDatasourceUtils.JDBC_DRIVER;
import static com.external.plugins.utils.OracleDatasourceUtils.createConnectionPool;
import static com.external.plugins.utils.OracleDatasourceUtils.getConnectionFromConnectionPool;
import static com.external.plugins.utils.OracleDatasourceUtils.logHikariCPStatus;
import static com.external.plugins.utils.OracleExecuteUtils.closeConnectionPostExecution;
import static com.external.plugins.utils.OracleExecuteUtils.isPLSQL;
@ -79,6 +79,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
@Slf4j
public class OraclePlugin extends BasePlugin {
public static final OracleDatasourceUtils oracleDatasourceUtils = new OracleDatasourceUtils();
public OraclePlugin(PluginWrapper wrapper) {
super(wrapper);
@ -197,7 +198,9 @@ public class OraclePlugin extends BasePlugin {
Connection connectionFromPool;
try {
connectionFromPool = getConnectionFromConnectionPool(connectionPool);
connectionFromPool =
oracleDatasourceUtils.getConnectionFromHikariConnectionPool(connectionPool,
ORACLE_PLUGIN_NAME);
} catch (SQLException | StaleConnectionException e) {
// The function can throw either StaleConnectionException or SQLException. The underlying hikari
// library throws SQLException in case the pool is closed or there is an issue initializing
@ -205,7 +208,8 @@ public class OraclePlugin extends BasePlugin {
// and should then trigger the destruction and recreation of the pool.
log.debug("Exception Occurred while getting connection from pool" + e.getMessage());
e.printStackTrace(System.out);
return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException());
return Mono.error(e instanceof StaleConnectionException ? e :
new StaleConnectionException(e.getMessage()));
}
List<Map<String, Object>> rowsList = new ArrayList<>(50);

View File

@ -1,9 +1,11 @@
package com.external.plugins.exceptions;
public class OracleErrorMessages {
private OracleErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class OracleErrorMessages extends BasePluginErrorMessages {
public static final String MISSING_QUERY_ERROR_MSG = "Missing required parameter: Query.";
public static final String QUERY_EXECUTION_FAILED_ERROR_MSG = "Your Oracle query failed to execute.";
@ -34,8 +36,6 @@ public class OracleErrorMessages {
public static final String DS_INVALID_HOSTNAME_ERROR_MSG = "Host value cannot contain `/` or `:` characters. Found `%s`.";
public static final String DS_MISSING_CONNECTION_MODE_ERROR_MSG = "Missing connection mode.";
public static final String DS_MISSING_AUTHENTICATION_DETAILS_ERROR_MSG = "Missing authentication details.";
public static final String DS_MISSING_USERNAME_ERROR_MSG = "Missing username for authentication.";

View File

@ -32,8 +32,14 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static com.appsmith.external.constants.PluginConstants.PluginName.ORACLE_PLUGIN_NAME;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_CLOSED_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NOT_RUNNING_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NULL_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.UNKNOWN_CONNECTION_ERROR_MSG;
import static com.appsmith.external.helpers.PluginUtils.safelyCloseSingleConnectionFromHikariCP;
import static com.external.plugins.OraclePlugin.OraclePluginExecutor.scheduler;
import static com.external.plugins.OraclePlugin.oracleDatasourceUtils;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.springframework.util.CollectionUtils.isEmpty;
@ -155,13 +161,15 @@ public class OracleDatasourceUtils {
return Mono.fromSupplier(() -> {
Connection connectionFromPool;
try {
connectionFromPool = getConnectionFromConnectionPool(connectionPool);
connectionFromPool =
oracleDatasourceUtils.getConnectionFromHikariConnectionPool(connectionPool, ORACLE_PLUGIN_NAME);
} catch (SQLException | StaleConnectionException e) {
// The function can throw either StaleConnectionException or SQLException. The
// underlying hikari library throws SQLException in case the pool is closed or there is an issue
// initializing the connection pool which can also be translated in our world to
// StaleConnectionException and should then trigger the destruction and recreation of the pool.
return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException());
return Mono.error(e instanceof StaleConnectionException ? e :
new StaleConnectionException(e.getMessage()));
}
logHikariCPStatus("Before getting Oracle DB schema", connectionPool);
@ -439,21 +447,6 @@ public class OracleDatasourceUtils {
return datasource;
}
/**
* First checks if the connection pool is still valid. If yes, we fetch a connection from the pool and return
* In case a connection is not available in the pool, SQL Exception is thrown
*/
public static java.sql.Connection getConnectionFromConnectionPool(HikariDataSource connectionPool) throws SQLException {
if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) {
System.out.println(Thread.currentThread().getName() +
": Encountered stale connection pool in Oracle plugin. Reporting back.");
throw new StaleConnectionException();
}
return connectionPool.getConnection();
}
public static void logHikariCPStatus(String logPrefix, HikariDataSource connectionPool) {
HikariPoolMXBean poolProxy = connectionPool.getHikariPoolMXBean();
int idleConnections = poolProxy.getIdleConnections();
@ -463,4 +456,36 @@ public class OracleDatasourceUtils {
log.debug(MessageFormat.format("{0}: Hikari Pool stats : active - {1} , idle - {2}, awaiting - {3} , total - {4}",
logPrefix, activeConnections, idleConnections, threadsAwaitingConnection, totalConnections));
}
public void checkHikariCPConnectionPoolValidity(HikariDataSource connectionPool, String pluginName) throws StaleConnectionException {
if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) {
String printMessage = MessageFormat.format(Thread.currentThread().getName() +
": Encountered stale connection pool in {0} plugin. Reporting back.", pluginName);
System.out.println(printMessage);
if (connectionPool == null) {
throw new StaleConnectionException(CONNECTION_POOL_NULL_ERROR_MSG);
}
else if (connectionPool.isClosed()) {
throw new StaleConnectionException(CONNECTION_POOL_CLOSED_ERROR_MSG);
}
else if (!connectionPool.isRunning()) {
throw new StaleConnectionException(CONNECTION_POOL_NOT_RUNNING_ERROR_MSG);
}
else {
/**
* Ideally, code flow is never expected to reach here. However, this section has been added to catch
* those cases wherein a developer updates the parent if condition but does not update the nested
* if else conditions.
*/
throw new StaleConnectionException(UNKNOWN_CONNECTION_ERROR_MSG);
}
}
}
public Connection getConnectionFromHikariConnectionPool(HikariDataSource connectionPool,
String pluginName) throws SQLException {
checkHikariCPConnectionPoolValidity(connectionPool, pluginName);
return connectionPool.getConnection();
}
}

View File

@ -1,11 +1,23 @@
package com.external.plugins;
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
import com.external.plugins.exceptions.OraclePluginError;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.stream.Collectors;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_CLOSED_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NOT_RUNNING_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NULL_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.UNKNOWN_CONNECTION_ERROR_MSG;
import static com.external.plugins.OracleTestDBContainerManager.oracleDatasourceUtils;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class OraclePluginErrorsTest {
@Test
public void verifyUniquenessOfOraclePluginErrorCode() {
@ -15,4 +27,63 @@ public class OraclePluginErrorsTest {
.filter(appErrorCode -> appErrorCode.length() != 11 || !appErrorCode.startsWith("PE-ORC"))
.collect(Collectors.toList()).size() == 0);
}
/**
* Not repeating this test for other plugins because (1) their implementation is identical to this one. (2) We
* want to re-factor this identical code into a common method in future (#24763) - could not be done right now because
* of some package dependency issues.
*/
@Test
public void testStaleConnectionErrorHasUpstreamErrorWhenConnectionPoolIsNull() {
Exception exception = assertThrows(StaleConnectionException.class,
() -> oracleDatasourceUtils.checkHikariCPConnectionPoolValidity(null
, "pluginName"));
String expectedErrorMessage = CONNECTION_POOL_NULL_ERROR_MSG;
assertEquals(expectedErrorMessage, exception.getMessage());
}
/**
* Not repeating this test for other plugins because (1) their implementation is identical to this one. (2) We
* want to re-factor this identical code into a common method in future (#24763) - could not be done right now because
* of some package dependency issues.
*/
@Test
public void testStaleConnectionErrorHasUpstreamErrorWhenConnectionPoolIsClosed() {
HikariDataSource mockConnectionPool = mock(HikariDataSource.class);
when(mockConnectionPool.isClosed()).thenReturn(true).thenReturn(true);
Exception exception = assertThrows(StaleConnectionException.class,
() -> oracleDatasourceUtils.checkHikariCPConnectionPoolValidity(mockConnectionPool, "pluginName"));
String expectedErrorMessage = CONNECTION_POOL_CLOSED_ERROR_MSG;
assertEquals(expectedErrorMessage, exception.getMessage());
}
/**
* Not repeating this test for other plugins because (1) their implementation is identical to this one. (2) We
* want to re-factor this identical code into a common method in future (#24763) - could not be done right now because
* of some package dependency issues.
*/
@Test
public void testStaleConnectionErrorHasUpstreamErrorWhenConnectionPoolIsRunning() {
HikariDataSource mockConnectionPool = mock(HikariDataSource.class);
when(mockConnectionPool.isRunning()).thenReturn(false).thenReturn(false);
Exception exception = assertThrows(StaleConnectionException.class,
() -> oracleDatasourceUtils.checkHikariCPConnectionPoolValidity(mockConnectionPool, "pluginName"));
String expectedErrorMessage = CONNECTION_POOL_NOT_RUNNING_ERROR_MSG;
assertEquals(expectedErrorMessage, exception.getMessage());
}
/**
* Not repeating this test for other plugins because (1) their implementation is identical to this one. (2) We
* want to re-factor this identical code into a common method in future (#24763) - could not be done right now because
* of some package dependency issues.
*/
@Test
public void testStaleConnectionErrorHasDefaultUpstreamError() {
HikariDataSource mockConnectionPool = mock(HikariDataSource.class);
when(mockConnectionPool.isRunning()).thenReturn(false).thenReturn(true);
Exception exception = assertThrows(StaleConnectionException.class,
() -> oracleDatasourceUtils.checkHikariCPConnectionPoolValidity(mockConnectionPool, "pluginName"));
String expectedErrorMessage = UNKNOWN_CONNECTION_ERROR_MSG;
assertEquals(expectedErrorMessage, exception.getMessage());
}
}

View File

@ -5,6 +5,7 @@ import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Endpoint;
import com.appsmith.external.models.SSLDetails;
import com.external.plugins.utils.OracleDatasourceUtils;
import com.zaxxer.hikari.HikariDataSource;
import org.testcontainers.containers.OracleContainer;
@ -12,7 +13,7 @@ import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import static com.external.plugins.utils.OracleDatasourceUtils.getConnectionFromConnectionPool;
import static com.appsmith.external.constants.PluginConstants.PluginName.ORACLE_PLUGIN_NAME;
import static com.external.plugins.utils.OracleExecuteUtils.closeConnectionPostExecution;
public class OracleTestDBContainerManager {
@ -20,6 +21,8 @@ public class OracleTestDBContainerManager {
public static final String ORACLE_PASSWORD = "testPassword";
public static final String ORACLE_DB_NAME = "testDB";
public static final String ORACLE_DOCKER_HUB_CONTAINER = "gvenzl/oracle-xe:21-slim-faststart";
public static OracleDatasourceUtils oracleDatasourceUtils = new OracleDatasourceUtils();
static OraclePlugin.OraclePluginExecutor oraclePluginExecutor = new OraclePlugin.OraclePluginExecutor();
public static OracleContainer getOracleDBForTest() {
@ -49,7 +52,9 @@ public class OracleTestDBContainerManager {
}
static void runSQLQueryOnOracleTestDB(String sqlQuery, HikariDataSource sharedConnectionPool) throws SQLException {
java.sql.Connection connectionFromPool = getConnectionFromConnectionPool(sharedConnectionPool);
java.sql.Connection connectionFromPool =
oracleDatasourceUtils.getConnectionFromHikariConnectionPool(sharedConnectionPool,
ORACLE_PLUGIN_NAME);
Statement statement = connectionFromPool.createStatement();
statement.execute(sqlQuery);
closeConnectionPostExecution(null, statement, null, connectionFromPool);

View File

@ -28,6 +28,7 @@ import com.appsmith.external.services.SharedConfig;
import com.external.plugins.datatypes.PostgresSpecificDataTypes;
import com.external.plugins.exceptions.PostgresErrorMessages;
import com.external.plugins.exceptions.PostgresPluginError;
import com.external.plugins.utils.PostgresDatasourceUtils;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean;
@ -76,6 +77,7 @@ import java.util.stream.IntStream;
import java.util.stream.Stream;
import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY;
import static com.appsmith.external.constants.PluginConstants.PluginName.POSTGRES_PLUGIN_NAME;
import static com.appsmith.external.helpers.PluginUtils.getColumnsListForJdbcPlugin;
import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns;
import static com.appsmith.external.helpers.PluginUtils.getPSParamLabel;
@ -124,13 +126,14 @@ public class PostgresPlugin extends BasePlugin {
private static int MAX_SIZE_SUPPORTED;
public static PostgresDatasourceUtils postgresDatasourceUtils = new PostgresDatasourceUtils();
public PostgresPlugin(PluginWrapper wrapper) {
super(wrapper);
}
@Extension
public static class PostgresPluginExecutor implements SmartSubstitutionInterface, PluginExecutor<HikariDataSource> {
private final Scheduler scheduler = Schedulers.boundedElastic();
private static final String TABLES_QUERY = "select a.attname as name,\n"
@ -281,7 +284,8 @@ public class PostgresPlugin extends BasePlugin {
Connection connectionFromPool;
try {
connectionFromPool = getConnectionFromConnectionPool(connection, datasourceConfiguration);
connectionFromPool = postgresDatasourceUtils.getConnectionFromHikariConnectionPool(connection,
POSTGRES_PLUGIN_NAME);
} catch (SQLException | StaleConnectionException e) {
// The function can throw either StaleConnectionException or SQLException. The
// underlying hikari
@ -290,7 +294,7 @@ public class PostgresPlugin extends BasePlugin {
// the connection pool which can also be translated in our world to
// StaleConnectionException
// and should then trigger the destruction and recreation of the pool.
return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException());
return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException(e.getMessage()));
}
List<Map<String, Object>> rowsList = new ArrayList<>(50);
@ -631,7 +635,8 @@ public class PostgresPlugin extends BasePlugin {
Connection connectionFromPool;
try {
connectionFromPool = getConnectionFromConnectionPool(connection, datasourceConfiguration);
connectionFromPool = postgresDatasourceUtils.getConnectionFromHikariConnectionPool(connection,
POSTGRES_PLUGIN_NAME);
} catch (SQLException | StaleConnectionException e) {
// The function can throw either StaleConnectionException or SQLException. The
// underlying hikari
@ -640,7 +645,8 @@ public class PostgresPlugin extends BasePlugin {
// the connection pool which can also be translated in our world to
// StaleConnectionException
// and should then trigger the destruction and recreation of the pool.
return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException());
return Mono.error(e instanceof StaleConnectionException ? e :
new StaleConnectionException(e.getMessage()));
}
HikariPoolMXBean poolProxy = connection.getHikariPoolMXBean();
@ -1101,30 +1107,4 @@ public class PostgresPlugin extends BasePlugin {
return datasource;
}
/**
* First checks if the connection pool is still valid. If yes, we fetch a
* connection from the pool and return
* In case a connection is not available in the pool, SQL Exception is thrown
*
* @param connectionPool
* @return SQL Connection
*/
private static Connection getConnectionFromConnectionPool(HikariDataSource connectionPool,
DatasourceConfiguration datasourceConfiguration) throws SQLException {
if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) {
log.debug("Encountered stale connection pool in Postgres plugin. Reporting back.");
throw new StaleConnectionException();
}
Connection connection = connectionPool.getConnection();
com.appsmith.external.models.Connection configurationConnection = datasourceConfiguration.getConnection();
if (configurationConnection == null) {
return connection;
}
return connection;
}
}

View File

@ -1,9 +1,11 @@
package com.external.plugins.exceptions;
public class PostgresErrorMessages {
private PostgresErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class PostgresErrorMessages extends BasePluginErrorMessages {
public static final String MISSING_QUERY_ERROR_MSG = "Missing required parameter: Query.";
public static final String QUERY_EXECUTION_FAILED_ERROR_MSG = "Your PostgreSQL query failed to execute.";

View File

@ -0,0 +1,47 @@
package com.external.plugins.utils;
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.MessageFormat;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_CLOSED_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NOT_RUNNING_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NULL_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.UNKNOWN_CONNECTION_ERROR_MSG;
public class PostgresDatasourceUtils {
public void checkHikariCPConnectionPoolValidity(HikariDataSource connectionPool, String pluginName) throws StaleConnectionException {
if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) {
String printMessage = MessageFormat.format(Thread.currentThread().getName() +
": Encountered stale connection pool in {0} plugin. Reporting back.", pluginName);
System.out.println(printMessage);
if (connectionPool == null) {
throw new StaleConnectionException(CONNECTION_POOL_NULL_ERROR_MSG);
}
else if (connectionPool.isClosed()) {
throw new StaleConnectionException(CONNECTION_POOL_CLOSED_ERROR_MSG);
}
else if (!connectionPool.isRunning()) {
throw new StaleConnectionException(CONNECTION_POOL_NOT_RUNNING_ERROR_MSG);
}
else {
/**
* Ideally, code flow is never expected to reach here. However, this section has been added to catch
* those cases wherein a developer updates the parent if condition but does not update the nested
* if else conditions.
*/
throw new StaleConnectionException(UNKNOWN_CONNECTION_ERROR_MSG);
}
}
}
public Connection getConnectionFromHikariConnectionPool(HikariDataSource connectionPool,
String pluginName) throws SQLException {
checkHikariCPConnectionPoolValidity(connectionPool, pluginName);
return connectionPool.getConnection();
}
}

View File

@ -1,10 +1,11 @@
package com.external.plugins.exceptions;
public class RedisErrorMessages {
private RedisErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class RedisErrorMessages extends BasePluginErrorMessages {
public static final String BODY_IS_NULL_OR_EMPTY_ERROR_MSG = "Body is null or empty [%s]";
public static final String QUERY_PARSING_FAILED_ERROR_MSG = "Appsmith server has failed to parse your Redis query. Are you sure it's" +

View File

@ -28,7 +28,6 @@
<groupId>com.amazon.redshift</groupId>
<artifactId>redshift-jdbc42</artifactId>
<version>2.1.0.9</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>

View File

@ -15,6 +15,7 @@ import com.appsmith.external.plugins.BasePlugin;
import com.appsmith.external.plugins.PluginExecutor;
import com.external.plugins.exceptions.RedshiftErrorMessages;
import com.external.plugins.exceptions.RedshiftPluginError;
import com.external.utils.RedshiftDatasourceUtils;
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean;
import lombok.NonNull;
@ -47,15 +48,17 @@ import java.util.Set;
import java.util.stream.Collectors;
import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY;
import static com.appsmith.external.constants.PluginConstants.PluginName.REDSHIFT_PLUGIN_NAME;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.JDBC_DRIVER_LOADING_ERROR_MSG;
import static com.appsmith.external.helpers.PluginUtils.getColumnsListForJdbcPlugin;
import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns;
import static com.external.utils.RedshiftDatasourceUtils.createConnectionPool;
import static com.external.utils.RedshiftDatasourceUtils.getConnectionFromConnectionPool;
@Slf4j
public class RedshiftPlugin extends BasePlugin {
public static final String JDBC_DRIVER = "com.amazon.redshift.jdbc.Driver";
private static final String DATE_COLUMN_TYPE_NAME = "date";
public static RedshiftDatasourceUtils redshiftDatasourceUtils = new RedshiftDatasourceUtils();
public RedshiftPlugin(PluginWrapper wrapper) {
super(wrapper);
@ -212,7 +215,7 @@ public class RedshiftPlugin extends BasePlugin {
return Mono.fromCallable(() -> {
Connection connection = null;
try {
connection = getConnectionFromConnectionPool(connectionPool);
connection = redshiftDatasourceUtils.getConnectionFromHikariConnectionPool(connectionPool, REDSHIFT_PLUGIN_NAME);
} catch (SQLException | StaleConnectionException e) {
e.printStackTrace();
@ -230,13 +233,13 @@ public class RedshiftPlugin extends BasePlugin {
// library throws SQLException in case the pool is closed or there is an issue initializing
// the connection pool which can also be translated in our world to StaleConnectionException
// and should then trigger the destruction and recreation of the pool.
return Mono.error(new StaleConnectionException());
return Mono.error(new StaleConnectionException(e.getMessage()));
}
/**
* Keeping this print statement post call to getConnectionFromConnectionPool because it checks for
* stale connection pool.
* Keeping this print statement post call to getConnectionFromHikariConnectionPool because it
* checks for stale connection pool.
*/
printConnectionPoolStatus(connectionPool, false);
@ -359,7 +362,7 @@ public class RedshiftPlugin extends BasePlugin {
try {
Class.forName(JDBC_DRIVER);
} catch (ClassNotFoundException e) {
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, RedshiftErrorMessages.JDBC_DRIVER_LOADING_ERROR_MSG, e.getMessage()));
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, JDBC_DRIVER_LOADING_ERROR_MSG, e.getMessage()));
}
return Mono
@ -569,7 +572,8 @@ public class RedshiftPlugin extends BasePlugin {
return Mono.fromSupplier(() -> {
Connection connection = null;
try {
connection = getConnectionFromConnectionPool(connectionPool);
connection = redshiftDatasourceUtils.getConnectionFromHikariConnectionPool(connectionPool
, REDSHIFT_PLUGIN_NAME);
} catch (SQLException | StaleConnectionException e) {
e.printStackTrace();
@ -587,7 +591,7 @@ public class RedshiftPlugin extends BasePlugin {
// library throws SQLException in case the pool is closed or there is an issue initializing
// the connection pool which can also be translated in our world to StaleConnectionException
// and should then trigger the destruction and recreation of the pool.
return Mono.error(new StaleConnectionException());
return Mono.error(new StaleConnectionException(e.getMessage()));
}
/**

View File

@ -1,10 +1,11 @@
package com.external.plugins.exceptions;
public class RedshiftErrorMessages {
private RedshiftErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class RedshiftErrorMessages extends BasePluginErrorMessages {
public static final String QUERY_PARAMETER_MISSING_ERROR_MSG = "Missing required parameter: Query.";
public static final String NULL_RESULTSET_ERROR_MSG = "Redshift driver failed to fetch result: resultSet is null.";
@ -18,7 +19,5 @@ public class RedshiftErrorMessages {
public static final String GET_STRUCTURE_ERROR_MSG = "Appsmith server has failed to fetch the structure of the database. "
+ "Please check if the database credentials are valid and/or you have the required permissions.";
public static final String JDBC_DRIVER_LOADING_ERROR_MSG = "Error loading Redshift JDBC Driver class.";
public static final String CONNECTION_POOL_CREATION_FAILED_ERROR_MSG = "Exception occurred while creating connection pool. One or more arguments in the datasource configuration may be invalid. Please check your datasource configuration.";
}

View File

@ -14,9 +14,14 @@ import org.springframework.util.StringUtils;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.List;
import java.util.stream.Collectors;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_CLOSED_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NOT_RUNNING_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NULL_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.UNKNOWN_CONNECTION_ERROR_MSG;
import static com.external.plugins.RedshiftPlugin.JDBC_DRIVER;
public class RedshiftDatasourceUtils {
@ -95,14 +100,35 @@ public class RedshiftDatasourceUtils {
return datasource;
}
public static Connection getConnectionFromConnectionPool(HikariDataSource connectionPool) throws SQLException {
public void checkHikariCPConnectionPoolValidity(HikariDataSource connectionPool, String pluginName) throws StaleConnectionException {
if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) {
System.out.println(Thread.currentThread().getName() +
": Encountered stale connection pool in Redshift plugin. Reporting back.");
throw new StaleConnectionException();
}
String printMessage = MessageFormat.format(Thread.currentThread().getName() +
": Encountered stale connection pool in {0} plugin. Reporting back.", pluginName);
System.out.println(printMessage);
if (connectionPool == null) {
throw new StaleConnectionException(CONNECTION_POOL_NULL_ERROR_MSG);
}
else if (connectionPool.isClosed()) {
throw new StaleConnectionException(CONNECTION_POOL_CLOSED_ERROR_MSG);
}
else if (!connectionPool.isRunning()) {
throw new StaleConnectionException(CONNECTION_POOL_NOT_RUNNING_ERROR_MSG);
}
else {
/**
* Ideally, code flow is never expected to reach here. However, this section has been added to catch
* those cases wherein a developer updates the parent if condition but does not update the nested
* if else conditions.
*/
throw new StaleConnectionException(UNKNOWN_CONNECTION_ERROR_MSG);
}
}
}
public Connection getConnectionFromHikariConnectionPool(HikariDataSource connectionPool,
String pluginName) throws SQLException {
checkHikariCPConnectionPoolValidity(connectionPool, pluginName);
return connectionPool.getConnection();
}
}

View File

@ -1,9 +1,11 @@
package com.external.plugins.exceptions;
public class RestApiErrorMessages {
private RestApiErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class RestApiErrorMessages extends BasePluginErrorMessages {
public static final String URI_SYNTAX_WRONG_ERROR_MSG = "Invalid value of URI.";
public static final String INVALID_CONTENT_TYPE_ERROR_MSG = "Invalid value for Content-Type.";
public static final String NO_HTTP_METHOD_ERROR_MSG = "HTTPMethod must be set.";

View File

@ -1,10 +1,11 @@
package com.external.plugins.exceptions;
public class SaaSErrorMessages {
private SaaSErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class SaaSErrorMessages extends BasePluginErrorMessages {
public static final String MISSING_DATASOURCE_TEMPLATE_NAME_ERROR_MSG = "Missing template name for datasource";
public static final String MISSING_ACTION_TEMPLATE_NAME_ERROR_MSG = "Missing template name for action";

View File

@ -1,10 +1,10 @@
package com.external.plugins.exceptions;
public class SMTPErrorMessages {
private SMTPErrorMessages() {
//Prevents instantiation
}
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class SMTPErrorMessages {
public static final String RECIPIENT_ADDRESS_NOT_FOUND_ERROR_MSG = "Couldn't find a valid recipient address. Please check your action configuration.";
public static final String SENDER_ADDRESS_NOT_FOUND_ERROR_MSG = "Couldn't find a valid sender address. Please check your action configuration.";

View File

@ -13,7 +13,6 @@ import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.plugins.BasePlugin;
import com.appsmith.external.plugins.PluginExecutor;
import com.external.plugins.exceptions.SnowflakeErrorMessages;
import com.external.plugins.exceptions.SnowflakePluginError;
import com.external.utils.SqlUtils;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
@ -22,7 +21,6 @@ import com.zaxxer.hikari.pool.HikariPool;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.Extension;
import org.pf4j.PluginWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
@ -41,7 +39,9 @@ import java.util.Map;
import java.util.Properties;
import java.util.Set;
import static com.appsmith.external.constants.PluginConstants.PluginName.SNOWFLAKE_PLUGIN_NAME;
import static com.external.utils.ExecutionUtils.getRowsFromQueryResult;
import static com.external.utils.SnowflakeDatasourceUtils.getConnectionFromHikariConnectionPool;
import static com.external.utils.ValidationUtils.validateWarehouseDatabaseSchema;
@Slf4j
@ -83,12 +83,19 @@ public class SnowflakePlugin extends BasePlugin {
Connection connectionFromPool;
try {
connectionFromPool = getConnectionFromConnectionPool(connection);
/**
* The getConnectionFromHikariConnectionPool method used here is the duplicate of
* method defined in PluginUtils.java and not the same one. Please check the comment on
* the method definition to understand more.
*/
connectionFromPool =
getConnectionFromHikariConnectionPool(connection,
SNOWFLAKE_PLUGIN_NAME);
} catch (SQLException | StaleConnectionException e) {
if (e instanceof StaleConnectionException) {
throw e;
} else {
throw new StaleConnectionException();
throw new StaleConnectionException(e.getMessage());
}
}
@ -275,7 +282,14 @@ public class SnowflakePlugin extends BasePlugin {
Connection connectionFromPool;
try {
connectionFromPool = getConnectionFromConnectionPool(connectionPool);
/**
* The getConnectionFromHikariConnectionPool method used here is the duplicate of
* method defined in PluginUtils.java and not the same one. Please check the comment on
* the method definition to understand more.
*/
connectionFromPool =
getConnectionFromHikariConnectionPool(connectionPool,
SNOWFLAKE_PLUGIN_NAME);
return Mono.just(validateWarehouseDatabaseSchema(connectionFromPool));
} catch (SQLException e) {
// The function can throw either StaleConnectionException or SQLException. The underlying hikari
@ -313,7 +327,13 @@ public class SnowflakePlugin extends BasePlugin {
Connection connectionFromPool;
try {
connectionFromPool = getConnectionFromConnectionPool(connection);
/**
* The getConnectionFromHikariConnectionPool method used here is the duplicate of
* method defined in PluginUtils.java and not the same one. Please check the comment on
* the method definition to understand more.
*/
connectionFromPool =
getConnectionFromHikariConnectionPool(connection, SNOWFLAKE_PLUGIN_NAME);
} catch (SQLException | StaleConnectionException e) {
// The function can throw either StaleConnectionException or SQLException. The underlying hikari
// library throws SQLException in case the pool is closed or there is an issue initializing
@ -391,22 +411,5 @@ public class SnowflakePlugin extends BasePlugin {
})
.subscribeOn(scheduler);
}
/**
* First checks if the connection pool is still valid. If yes, we fetch a connection from the pool and return
* In case a connection is not available in the pool, SQL Exception is thrown
*
* @param connectionPool
* @return SQL Connection
*/
private static Connection getConnectionFromConnectionPool(HikariDataSource connectionPool) throws SQLException {
if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) {
log.debug("Encountered stale connection pool in Snowflake plugin. Reporting back.");
throw new StaleConnectionException();
}
return connectionPool.getConnection();
}
}
}

View File

@ -1,17 +1,15 @@
package com.external.plugins.exceptions;
public class SnowflakeErrorMessages {
private SnowflakeErrorMessages() {
//Prevents instantiation
}
import com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE) // To prevent instantiation
public class SnowflakeErrorMessages extends BasePluginErrorMessages {
public static final String MISSING_QUERY_ERROR_MSG = "Missing required parameter: Query.";
public static final String DRIVER_NOT_FOUND_ERROR_MSG = "Snowflake driver not found. Please reach out to Appsmith support to resolve this issue.";
public static final String QUERY_EXECUTION_FAILED_ERROR_MSG = "Your query failed to execute. Please check more information in the error details.";
public static final String CONNECTION_CREATION_FAILED_ERROR_MSG = "Error occurred while connecting to Snowflake endpoint: %s";
public static final String UNABLE_TO_CREATE_CONNECTION_ERROR_MSG = "Unable to create connection to Snowflake URL";
public static final String GET_STRUCTURE_ERROR_MSG = "Appsmith server has failed to fetch the structure of your schema. Please check more information in the error details.";

View File

@ -17,6 +17,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static com.external.plugins.exceptions.SnowflakeErrorMessages.CONNECTION_INVALID_ERROR_MSG;
@Slf4j
public class ExecutionUtils {
/**
@ -38,7 +40,7 @@ public class ExecutionUtils {
// Instead for every execution, we check for connection validity,
// and reset the connection if required
if (!connection.isValid(30)) {
throw new StaleConnectionException();
throw new StaleConnectionException(CONNECTION_INVALID_ERROR_MSG);
}
statement = connection.createStatement();
@ -58,7 +60,7 @@ public class ExecutionUtils {
}
} catch (SQLException e) {
if (e instanceof SnowflakeReauthenticationRequest) {
throw new StaleConnectionException();
throw new StaleConnectionException(e.getMessage());
}
log.error("Exception caught when executing Snowflake query. Cause: ", e);
throw new AppsmithPluginException(SnowflakePluginError.QUERY_EXECUTION_FAILED, SnowflakeErrorMessages.QUERY_EXECUTION_FAILED_ERROR_MSG, e.getMessage(), "SQLSTATE: " + e.getSQLState() );

View File

@ -0,0 +1,47 @@
package com.external.utils;
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.MessageFormat;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_CLOSED_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NOT_RUNNING_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.CONNECTION_POOL_NULL_ERROR_MSG;
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.UNKNOWN_CONNECTION_ERROR_MSG;
public class SnowflakeDatasourceUtils {
public static void checkHikariCPConnectionPoolValidity(HikariDataSource connectionPool, String pluginName) throws StaleConnectionException {
if (connectionPool == null || connectionPool.isClosed() || !connectionPool.isRunning()) {
String printMessage = MessageFormat.format(Thread.currentThread().getName() +
": Encountered stale connection pool in {0} plugin. Reporting back.", pluginName);
System.out.println(printMessage);
if (connectionPool == null) {
throw new StaleConnectionException(CONNECTION_POOL_NULL_ERROR_MSG);
}
else if (connectionPool.isClosed()) {
throw new StaleConnectionException(CONNECTION_POOL_CLOSED_ERROR_MSG);
}
else if (!connectionPool.isRunning()) {
throw new StaleConnectionException(CONNECTION_POOL_NOT_RUNNING_ERROR_MSG);
}
else {
/**
* Ideally, code flow is never expected to reach here. However, this section has been added to catch
* those cases wherein a developer updates the parent if condition but does not update the nested
* if else conditions.
*/
throw new StaleConnectionException(UNKNOWN_CONNECTION_ERROR_MSG);
}
}
}
public static Connection getConnectionFromHikariConnectionPool(HikariDataSource connectionPool,
String pluginName) throws SQLException {
checkHikariCPConnectionPoolValidity(connectionPool, pluginName);
return connectionPool.getConnection();
}
}

View File

@ -644,7 +644,7 @@ public class ActionExecutionSolutionCEImpl implements ActionExecutionSolutionCE
return new AppsmithPluginException(AppsmithPluginError.PLUGIN_QUERY_TIMEOUT_ERROR,
actionDTO.getName(), timeoutDuration);
} else if (error instanceof StaleConnectionException e) {
return new AppsmithPluginException(AppsmithPluginError.STALE_CONNECTION_ERROR);
return new AppsmithPluginException(AppsmithPluginError.STALE_CONNECTION_ERROR, e.getMessage());
} else {
return error;
}