From f9cca885ed8b18de339f26f91181f7d6ad48009f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 5 Jun 2024 15:23:02 -0500 Subject: [PATCH] checkpoint - working version of c3p0 connection pooling, and read-only database meta-data connections (per query hint) --- .../core/actions/reporting/ExportAction.java | 4 +- .../reporting/GenerateReportAction.java | 4 +- .../values/QPossibleValueTranslator.java | 44 +-- .../core/model/actions/tables/QueryHint.java | 38 ++ .../tables/aggregate/AggregateInput.java | 78 +++++ .../actions/tables/count/CountInput.java | 78 +++++ .../actions/tables/query/QueryInput.java | 35 +- .../GarbageCollectorExtractStep.java | 3 +- .../rdbms/actions/AbstractRDBMSAction.java | 33 +- .../rdbms/actions/RDBMSQueryAction.java | 4 +- .../jdbc/C3P0PooledConnectionProvider.java | 182 ++++++++++ .../module/rdbms/jdbc/ConnectionManager.java | 126 ++++--- .../jdbc/ConnectionProviderInterface.java | 58 ++++ .../rdbms/jdbc/SimpleConnectionProvider.java | 63 ++++ .../metadata/ConnectionPoolSettings.java | 326 ++++++++++++++++++ .../model/metadata/RDBMSBackendMetaData.java | 100 ++++++ .../C3P0PooledConnectionProviderTest.java | 237 +++++++++++++ .../javalin/QJavalinImplementation.java | 3 + 18 files changed, 1294 insertions(+), 122 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/QueryHint.java create mode 100644 qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProvider.java create mode 100644 qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionProviderInterface.java create mode 100644 qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/SimpleConnectionProvider.java create mode 100644 qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/ConnectionPoolSettings.java create mode 100644 qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProviderTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java index e14de682..9c5628f9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java @@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; @@ -216,7 +217,8 @@ public class ExportAction } queryInput.getFilter().setLimit(exportInput.getLimit()); queryInput.setShouldTranslatePossibleValues(true); - queryInput.withQueryHint(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS); + queryInput.withQueryHint(QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS); + queryInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); ///////////////////////////////////////////////////////////////// // tell this query that it needs to put its output into a pipe // diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java index 3c2ace5c..a984bed3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java @@ -59,6 +59,7 @@ import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; @@ -417,7 +418,8 @@ public class GenerateReportAction extends AbstractQActionFunction transactionsPerTable = new HashMap<>(); - - // todo not commit - remove instance & session - use Context - - - boolean useTransactionsAsConnectionPool = false; - - - - /******************************************************************************* - ** - *******************************************************************************/ - private QBackendTransaction getTransaction(String tableName) - { - ///////////////////////////////////////////////////////////// - // mmm, this does cut down on connections used - // - // especially seems helpful in big exports. // - // but, let's just start using connection pools instead... // - ///////////////////////////////////////////////////////////// - if(useTransactionsAsConnectionPool) - { - try - { - if(!transactionsPerTable.containsKey(tableName)) - { - transactionsPerTable.put(tableName, QBackendTransaction.openFor(new InsertInput(tableName))); - } - - return (transactionsPerTable.get(tableName)); - } - catch(Exception e) - { - LOG.warn("Error opening transaction for table", logPair("tableName", tableName)); - } - } - - return null; - } - /******************************************************************************* @@ -601,7 +561,7 @@ public class QPossibleValueTranslator QueryInput queryInput = new QueryInput(); queryInput.setTableName(tableName); queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(idField, QCriteriaOperator.IN, page))); - queryInput.setTransaction(getTransaction(tableName)); + queryInput.hasQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // when querying for possible values, we do want to generate their display values, which makes record labels, which are usually used as PVS labels // diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/QueryHint.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/QueryHint.java new file mode 100644 index 00000000..8c7050f2 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/QueryHint.java @@ -0,0 +1,38 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.actions.tables; + + +/******************************************************************************* + ** Information about the query that an application (or qqq service) may know and + ** want to tell the backend, that can help influence how the backend processes + ** query. + ** + ** For example, a query with potentially a large result set, for MySQL backend, + ** we may want to configure the result set to stream results rather than do its + ** default in-memory thing. See RDBMSQueryAction for usage. + *******************************************************************************/ +public enum QueryHint +{ + POTENTIALLY_LARGE_NUMBER_OF_RESULTS, + MAY_USE_READ_ONLY_BACKEND +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateInput.java index 862ebcf6..9d69e5ac 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateInput.java @@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.aggregate; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; @@ -44,6 +46,8 @@ public class AggregateInput extends AbstractTableActionInput private List queryJoins = null; + private EnumSet queryHints = EnumSet.noneOf(QueryHint.class); + /******************************************************************************* @@ -302,4 +306,78 @@ public class AggregateInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for queryHints + *******************************************************************************/ + public EnumSet getQueryHints() + { + return (this.queryHints); + } + + + + /******************************************************************************* + ** Setter for queryHints + *******************************************************************************/ + public void setQueryHints(EnumSet queryHints) + { + this.queryHints = queryHints; + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public AggregateInput withQueryHints(EnumSet queryHints) + { + this.queryHints = queryHints; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public AggregateInput withQueryHint(QueryHint queryHint) + { + if(this.queryHints == null) + { + this.queryHints = EnumSet.noneOf(QueryHint.class); + } + this.queryHints.add(queryHint); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public AggregateInput withoutQueryHint(QueryHint queryHint) + { + if(this.queryHints != null) + { + this.queryHints.remove(queryHint); + } + return (this); + } + + + + /******************************************************************************* + ** null-safely check if query hints map contains the specified hint + *******************************************************************************/ + public boolean hasQueryHint(QueryHint queryHint) + { + if(this.queryHints == null) + { + return (false); + } + + return (queryHints.contains(queryHint)); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java index e1994d8b..a4f47090 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java @@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.count; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; @@ -42,6 +44,8 @@ public class CountInput extends AbstractTableActionInput private List queryJoins = null; private Boolean includeDistinctCount = false; + private EnumSet queryHints = EnumSet.noneOf(QueryHint.class); + /******************************************************************************* @@ -207,4 +211,78 @@ public class CountInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for queryHints + *******************************************************************************/ + public EnumSet getQueryHints() + { + return (this.queryHints); + } + + + + /******************************************************************************* + ** Setter for queryHints + *******************************************************************************/ + public void setQueryHints(EnumSet queryHints) + { + this.queryHints = queryHints; + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public CountInput withQueryHints(EnumSet queryHints) + { + this.queryHints = queryHints; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public CountInput withQueryHint(QueryHint queryHint) + { + if(this.queryHints == null) + { + this.queryHints = EnumSet.noneOf(QueryHint.class); + } + this.queryHints.add(queryHint); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public CountInput withoutQueryHint(QueryHint queryHint) + { + if(this.queryHints != null) + { + this.queryHints.remove(queryHint); + } + return (this); + } + + + + /******************************************************************************* + ** null-safely check if query hints map contains the specified hint + *******************************************************************************/ + public boolean hasQueryHint(QueryHint queryHint) + { + if(this.queryHints == null) + { + return (false); + } + + return (queryHints.contains(queryHint)); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java index a0e71e19..5a00b1a3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java @@ -31,12 +31,16 @@ import java.util.Set; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface; /******************************************************************************* ** Input data for the Query action ** + ** Todo - maybe make a class between AbstractTableActionInput and {QueryInput, + ** CountInput, and AggregateInput}, with common attributes for all of these + ** "read" operations (like, queryHints, *******************************************************************************/ public class QueryInput extends AbstractTableActionInput implements QueryOrGetInputInterface, Cloneable { @@ -74,22 +78,6 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn - /******************************************************************************* - ** Information about the query that an application (or qqq service) may know and - ** want to tell the backend, that can help influence how the backend processes - ** query. - ** - ** For example, a query with potentially a large result set, for MySQL backend, - ** we may want to configure the result set to stream results rather than do its - ** default in-memory thing. See RDBMSQueryAction for usage. - *******************************************************************************/ - public enum QueryHint - { - POTENTIALLY_LARGE_NUMBER_OF_RESULTS - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -683,4 +671,19 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn return (this); } + + + /******************************************************************************* + ** null-safely check if query hints map contains the specified hint + *******************************************************************************/ + public boolean hasQueryHint(QueryHint queryHint) + { + if(this.queryHints == null) + { + return (false); + } + + return (queryHints.contains(queryHint)); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/garbagecollector/GarbageCollectorExtractStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/garbagecollector/GarbageCollectorExtractStep.java index 3f4ee4c0..171b4c33 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/garbagecollector/GarbageCollectorExtractStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/garbagecollector/GarbageCollectorExtractStep.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.processes.implementations.garbagecollecto import java.time.Instant; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; @@ -66,7 +67,7 @@ public class GarbageCollectorExtractStep extends ExtractViaQueryStep @Override protected void customizeInputPreQuery(QueryInput queryInput) { - queryInput.withQueryHint(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS); + queryInput.withQueryHint(QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS); } } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index afb3b5d6..3940f31b 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -47,14 +47,18 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByGroupBy; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -135,11 +139,34 @@ public abstract class AbstractRDBMSAction /******************************************************************************* ** Get a database connection, per the backend in the request. + ** + ** Note that it may be a connection to a read-only backend, per query-hints, + ** and backend settings. *******************************************************************************/ - public static Connection getConnection(AbstractTableActionInput qTableRequest) throws SQLException + public static Connection getConnection(AbstractTableActionInput tableActionInput) throws SQLException { - ConnectionManager connectionManager = new ConnectionManager(); - return connectionManager.getConnection((RDBMSBackendMetaData) qTableRequest.getBackend()); + RDBMSBackendMetaData backend = (RDBMSBackendMetaData) tableActionInput.getBackend(); + + boolean useReadOnly = false; + if(tableActionInput instanceof QueryInput queryInput) + { + useReadOnly = queryInput.hasQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); + } + else if(tableActionInput instanceof CountInput countInput) + { + useReadOnly = countInput.hasQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); + } + else if(tableActionInput instanceof AggregateInput aggregateInput) + { + useReadOnly = aggregateInput.hasQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); + } + + if(useReadOnly && backend.getReadOnlyBackendMetaData() != null) + { + return ConnectionManager.getConnection(backend.getReadOnlyBackendMetaData()); + } + + return ConnectionManager.getConnection(backend); } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index 86f119fb..8bcb81b2 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; @@ -80,7 +81,6 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf } } - /******************************************************************************* ** *******************************************************************************/ @@ -361,7 +361,7 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // if we're allowed to use the mysqlResultSetOptimization, and we have the query hint of "expected large result set", then do it. // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(mysqlResultSetOptimizationEnabled && queryInput.getQueryHints() != null && queryInput.getQueryHints().contains(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS)) + if(mysqlResultSetOptimizationEnabled && queryInput.getQueryHints() != null && queryInput.getQueryHints().contains(QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS)) { ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // mysql "optimization", presumably here - from Result Set section of https://dev.mysql.com/doc/connector-j/en/connector-j-reference-implementation-notes.html // diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProvider.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProvider.java new file mode 100644 index 00000000..1a4740e4 --- /dev/null +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProvider.java @@ -0,0 +1,182 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.rdbms.jdbc; + + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.LinkedHashMap; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.ConnectionPoolSettings; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; +import com.mchange.v2.c3p0.ComboPooledDataSource; +import org.json.JSONObject; +import static com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager.getJdbcUrl; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class C3P0PooledConnectionProvider implements ConnectionProviderInterface +{ + private RDBMSBackendMetaData backend; + private ComboPooledDataSource connectionPool; + + private long usageCount = 0; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void init(RDBMSBackendMetaData backend) throws QException + { + this.backend = backend; + + try + { + ComboPooledDataSource pool = new ComboPooledDataSource(); + pool.setDriverClass(ConnectionManager.getJdbcDriverClassName(backend)); + pool.setJdbcUrl(getJdbcUrl(backend)); + pool.setUser(backend.getUsername()); + pool.setPassword(backend.getPassword()); + + ConnectionPoolSettings poolSettings = backend.getConnectionPoolSettings(); + if(poolSettings != null) + { + if(poolSettings.getInitialPoolSize() != null) + { + pool.setInitialPoolSize(poolSettings.getInitialPoolSize()); + } + + if(poolSettings.getMinPoolSize() != null) + { + pool.setMinPoolSize(poolSettings.getMinPoolSize()); + } + + if(poolSettings.getMaxPoolSize() != null) + { + pool.setMaxPoolSize(poolSettings.getMaxPoolSize()); + } + + if(poolSettings.getAcquireIncrement() != null) + { + pool.setAcquireIncrement(poolSettings.getAcquireIncrement()); + } + + if(poolSettings.getMaxConnectionAgeSeconds() != null) + { + pool.setMaxConnectionAge(poolSettings.getMaxConnectionAgeSeconds()); + } + + if(poolSettings.getMaxIdleTimeSeconds() != null) + { + pool.setMaxIdleTime(poolSettings.getMaxIdleTimeSeconds()); + } + + if(poolSettings.getMaxIdleTimeExcessConnectionsSeconds() != null) + { + pool.setMaxIdleTimeExcessConnections(poolSettings.getMaxIdleTimeExcessConnectionsSeconds()); + } + + if(poolSettings.getCheckoutTimeoutSeconds() != null) + { + pool.setCheckoutTimeout(poolSettings.getCheckoutTimeoutSeconds() * 1000); + } + + if(poolSettings.getTestConnectionOnCheckout() != null) + { + pool.setTestConnectionOnCheckout(poolSettings.getTestConnectionOnCheckout()); + } + } + + customizePool(pool); + + this.connectionPool = pool; + } + catch(Exception e) + { + throw (new QException("Error Initializing C3P0PooledConnectionProvider for backend [" + backend.getName() + "]", e)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected void customizePool(ComboPooledDataSource pool) + { + //////////////////////// + // noop in base class // + //////////////////////// + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Connection getConnection() throws SQLException + { + usageCount++; + return (this.connectionPool.getConnection()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public JSONObject dumpDebug() throws SQLException + { + JSONObject rs = new JSONObject(new LinkedHashMap<>()); + + JSONObject settings = new JSONObject(new LinkedHashMap<>()); + rs.put("settings", settings); + settings.put("initialPoolSize", connectionPool.getInitialPoolSize()); + settings.put("minPoolSize", connectionPool.getMinPoolSize()); + settings.put("maxPoolSize", connectionPool.getMaxPoolSize()); + settings.put("acquireIncrement", connectionPool.getAcquireIncrement()); + settings.put("maxConnectionAge", connectionPool.getMaxConnectionAge()); + settings.put("maxIdleTime", connectionPool.getMaxIdleTime()); + settings.put("maxIdleTimeExcessConnections", connectionPool.getMaxIdleTimeExcessConnections()); + settings.put("checkoutTimeout", connectionPool.getCheckoutTimeout()); + settings.put("testConnectionOnCheckout", connectionPool.isTestConnectionOnCheckout()); + + JSONObject state = new JSONObject(new LinkedHashMap<>()); + rs.put("state", state); + state.put("numUsages", usageCount); + state.put("numConnections", connectionPool.getNumConnections()); + state.put("numBusyConnections", connectionPool.getNumBusyConnections()); + state.put("numIdleConnections", connectionPool.getNumIdleConnections()); + state.put("numFailedCheckins", connectionPool.getNumFailedCheckinsDefaultUser()); + state.put("numFailedCheckouts", connectionPool.getNumFailedCheckoutsDefaultUser()); + state.put("numFailedIdleTests", connectionPool.getNumFailedIdleTestsDefaultUser()); + state.put("numThreadsAwaitingCheckout", connectionPool.getNumThreadsAwaitingCheckoutDefaultUser()); + return (rs); + } + +} diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java index 3800810f..688e231b 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java @@ -23,68 +23,81 @@ package com.kingsrook.qqq.backend.module.rdbms.jdbc; import java.sql.Connection; -import java.sql.DriverManager; import java.sql.SQLException; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import java.util.concurrent.ConcurrentHashMap; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; -import com.mchange.v2.c3p0.ComboPooledDataSource; +import org.json.JSONArray; +import org.json.JSONObject; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* + ** Class to manage access to JDBC Connections. ** + ** Relies heavily on RDBMSBackendMetaData. *******************************************************************************/ public class ConnectionManager { - private boolean mayUseConnectionPool = true; + private static final QLogger LOG = QLogger.getLogger(ConnectionManager.class); - private static Map initedConnectionPool = new HashMap<>(); - private static Map connectionPoolMap = new HashMap<>(); + private static final Map connectionProviderMap = new ConcurrentHashMap<>(); - private static int usageCounter = 0; /******************************************************************************* ** *******************************************************************************/ - public Connection getConnection(RDBMSBackendMetaData backend) throws SQLException - { - usageCounter++; - - if(mayUseConnectionPool) - { - return (getConnectionFromPool(backend)); - } - - String jdbcURL = getJdbcUrl(backend); - return DriverManager.getConnection(jdbcURL, backend.getUsername(), backend.getPassword()); - } - - - /******************************************************************************* - ** - *******************************************************************************/ - public static void checkPools() + public static Connection getConnection(RDBMSBackendMetaData backend) throws SQLException { try { - System.out.println("Usages: " + usageCounter); + ConnectionProviderInterface connectionProvider = getConnectionProvider(backend); + return connectionProvider.getConnection(); + } + catch(QException qe) + { + throw (new SQLException("Error getting connection", qe)); + } + } - for(Map.Entry entry : CollectionUtils.nonNullMap(connectionPoolMap).entrySet()) + + + /******************************************************************************* + ** + *******************************************************************************/ + private static ConnectionProviderInterface getConnectionProvider(RDBMSBackendMetaData backend) throws QException + { + if(!connectionProviderMap.containsKey(backend.getName())) + { + synchronized(connectionProviderMap) { - System.out.println("POOL USAGE: " + entry.getKey() + ": " + entry.getValue().getNumBusyConnections()); - if(entry.getValue().getNumBusyConnections() > 2) + if(!connectionProviderMap.containsKey(backend.getName())) { - System.out.println("break!"); + QCodeReference connectionProviderReference = backend.getConnectionProvider(); + boolean usingDefaultSimpleProvider = false; + if(connectionProviderReference == null) + { + connectionProviderReference = new QCodeReference(SimpleConnectionProvider.class); + usingDefaultSimpleProvider = true; + } + + LOG.info("Initializing connection provider for RDBMS backend", logPair("backendName", backend.getName()), logPair("connectionProvider", connectionProviderReference.getName()), logPair("usingDefaultSimpleProvider", usingDefaultSimpleProvider)); + ConnectionProviderInterface connectionProvider = QCodeLoader.getAdHoc(ConnectionProviderInterface.class, connectionProviderReference); + connectionProvider.init(backend); + + connectionProviderMap.put(backend.getName(), connectionProvider); } } } - catch(Exception e) - { - e.printStackTrace(); - } + + return (connectionProviderMap.get(backend.getName())); } @@ -92,36 +105,27 @@ public class ConnectionManager /******************************************************************************* ** *******************************************************************************/ - private Connection getConnectionFromPool(RDBMSBackendMetaData backend) throws SQLException + public static JSONArray dumpConnectionProviderDebug() { try { - if(!initedConnectionPool.getOrDefault(backend.getName(), false)) + JSONArray rs = new JSONArray(); + for(Map.Entry entry : connectionProviderMap.entrySet()) { - // todo - some syncrhonized - ComboPooledDataSource connectionPool = new ComboPooledDataSource(); - connectionPool.setDriverClass(getJdbcDriverClassName(backend)); - connectionPool.setJdbcUrl(getJdbcUrl(backend)); - connectionPool.setUser(backend.getUsername()); - connectionPool.setPassword(backend.getPassword()); - - connectionPool.setTestConnectionOnCheckout(true); - - ////////////////////////////////////////////////////////////////////////// - // useful to debug leaking connections - meant for tests only though... // - ////////////////////////////////////////////////////////////////////////// - // connectionPool.setDebugUnreturnedConnectionStackTraces(true); - // connectionPool.setUnreturnedConnectionTimeout(10); - - connectionPoolMap.put(backend.getName(), connectionPool); - initedConnectionPool.put(backend.getName(), true); + JSONObject jsonObject = new JSONObject(new LinkedHashMap<>()); + jsonObject.put("backendName", entry.getKey()); + jsonObject.put("connectionProviderClass", entry.getValue().getClass().getName()); + jsonObject.put("values", entry.getValue().dumpDebug()); + rs.put(jsonObject); } - return (connectionPoolMap.get(backend.getName()).getConnection()); + return (rs); } catch(Exception e) { - throw (new SQLException("Error getting connection from pool", e)); + String message = "Error dumping debug data for connection providers"; + LOG.warn(message, e); + return (new JSONArray(new JSONObject(Map.of("error", e.getMessage())))); } } @@ -168,4 +172,14 @@ public class ConnectionManager }; } + + + /******************************************************************************* + ** reset the map of connection providers - not necessarily meant to be useful + ** in production code - written for use in qqq tests. + *******************************************************************************/ + static void resetConnectionProviders() + { + connectionProviderMap.clear(); + } } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionProviderInterface.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionProviderInterface.java new file mode 100644 index 00000000..6e80f5d5 --- /dev/null +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionProviderInterface.java @@ -0,0 +1,58 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.rdbms.jdbc; + + +import java.sql.Connection; +import java.sql.SQLException; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; +import org.json.JSONObject; + + +/******************************************************************************* + ** interface for classes that can provide jdbc Connections for an RDBMS backend. + *******************************************************************************/ +public interface ConnectionProviderInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void init(RDBMSBackendMetaData backend) throws QException; + + /******************************************************************************* + ** + *******************************************************************************/ + Connection getConnection() throws SQLException; + + /******************************************************************************* + ** + *******************************************************************************/ + default JSONObject dumpDebug() throws SQLException + { + JSONObject rs = new JSONObject(); + rs.put("nothingToReport", true); + return (rs); + } +} + diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/SimpleConnectionProvider.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/SimpleConnectionProvider.java new file mode 100644 index 00000000..264f5528 --- /dev/null +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/SimpleConnectionProvider.java @@ -0,0 +1,63 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.rdbms.jdbc; + + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; +import static com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager.getJdbcUrl; + + +/******************************************************************************* + ** Simple connection provider - no pooling, just opens a new connection for + ** every request. + *******************************************************************************/ +public class SimpleConnectionProvider implements ConnectionProviderInterface +{ + private RDBMSBackendMetaData backend; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void init(RDBMSBackendMetaData backend) + { + this.backend = backend; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Connection getConnection() throws SQLException + { + String jdbcURL = getJdbcUrl(backend); + return DriverManager.getConnection(jdbcURL, backend.getUsername(), backend.getPassword()); + } + +} diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/ConnectionPoolSettings.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/ConnectionPoolSettings.java new file mode 100644 index 00000000..63a0d650 --- /dev/null +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/ConnectionPoolSettings.java @@ -0,0 +1,326 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.rdbms.model.metadata; + + +/******************************************************************************* + ** Settings for a connection pool (if your backend is configured to use one). + ** Originally based on the most common settings for C3P0 - see + ** https://www.mchange.com/projects/c3p0/#configuration + ** + ** If you want more - you'll be looking at defining your own subclass of + ** C3P0PooledConnectionProvider and possibly this class. + ** + ** If using a pool other than C3P0 - some of these may apply others may not. + *******************************************************************************/ +public class ConnectionPoolSettings +{ + private Integer initialPoolSize; + private Integer minPoolSize; + private Integer maxPoolSize; + private Integer acquireIncrement; + private Integer maxConnectionAgeSeconds; + private Integer maxIdleTimeSeconds; + private Integer maxIdleTimeExcessConnectionsSeconds; + private Integer checkoutTimeoutSeconds; + private Boolean testConnectionOnCheckout; + + + + /******************************************************************************* + ** Getter for initialPoolSize + *******************************************************************************/ + public Integer getInitialPoolSize() + { + return (this.initialPoolSize); + } + + + + /******************************************************************************* + ** Setter for initialPoolSize + *******************************************************************************/ + public void setInitialPoolSize(Integer initialPoolSize) + { + this.initialPoolSize = initialPoolSize; + } + + + + /******************************************************************************* + ** Fluent setter for initialPoolSize + *******************************************************************************/ + public ConnectionPoolSettings withInitialPoolSize(Integer initialPoolSize) + { + this.initialPoolSize = initialPoolSize; + return (this); + } + + + + /******************************************************************************* + ** Getter for minPoolSize + *******************************************************************************/ + public Integer getMinPoolSize() + { + return (this.minPoolSize); + } + + + + /******************************************************************************* + ** Setter for minPoolSize + *******************************************************************************/ + public void setMinPoolSize(Integer minPoolSize) + { + this.minPoolSize = minPoolSize; + } + + + + /******************************************************************************* + ** Fluent setter for minPoolSize + *******************************************************************************/ + public ConnectionPoolSettings withMinPoolSize(Integer minPoolSize) + { + this.minPoolSize = minPoolSize; + return (this); + } + + + + /******************************************************************************* + ** Getter for maxPoolSize + *******************************************************************************/ + public Integer getMaxPoolSize() + { + return (this.maxPoolSize); + } + + + + /******************************************************************************* + ** Setter for maxPoolSize + *******************************************************************************/ + public void setMaxPoolSize(Integer maxPoolSize) + { + this.maxPoolSize = maxPoolSize; + } + + + + /******************************************************************************* + ** Fluent setter for maxPoolSize + *******************************************************************************/ + public ConnectionPoolSettings withMaxPoolSize(Integer maxPoolSize) + { + this.maxPoolSize = maxPoolSize; + return (this); + } + + + + /******************************************************************************* + ** Getter for acquireIncrement + *******************************************************************************/ + public Integer getAcquireIncrement() + { + return (this.acquireIncrement); + } + + + + /******************************************************************************* + ** Setter for acquireIncrement + *******************************************************************************/ + public void setAcquireIncrement(Integer acquireIncrement) + { + this.acquireIncrement = acquireIncrement; + } + + + + /******************************************************************************* + ** Fluent setter for acquireIncrement + *******************************************************************************/ + public ConnectionPoolSettings withAcquireIncrement(Integer acquireIncrement) + { + this.acquireIncrement = acquireIncrement; + return (this); + } + + + + /******************************************************************************* + ** Getter for maxConnectionAgeSeconds + *******************************************************************************/ + public Integer getMaxConnectionAgeSeconds() + { + return (this.maxConnectionAgeSeconds); + } + + + + /******************************************************************************* + ** Setter for maxConnectionAgeSeconds + *******************************************************************************/ + public void setMaxConnectionAgeSeconds(Integer maxConnectionAgeSeconds) + { + this.maxConnectionAgeSeconds = maxConnectionAgeSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for maxConnectionAgeSeconds + *******************************************************************************/ + public ConnectionPoolSettings withMaxConnectionAgeSeconds(Integer maxConnectionAgeSeconds) + { + this.maxConnectionAgeSeconds = maxConnectionAgeSeconds; + return (this); + } + + + + /******************************************************************************* + ** Getter for maxIdleTimeSeconds + *******************************************************************************/ + public Integer getMaxIdleTimeSeconds() + { + return (this.maxIdleTimeSeconds); + } + + + + /******************************************************************************* + ** Setter for maxIdleTimeSeconds + *******************************************************************************/ + public void setMaxIdleTimeSeconds(Integer maxIdleTimeSeconds) + { + this.maxIdleTimeSeconds = maxIdleTimeSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for maxIdleTimeSeconds + *******************************************************************************/ + public ConnectionPoolSettings withMaxIdleTimeSeconds(Integer maxIdleTimeSeconds) + { + this.maxIdleTimeSeconds = maxIdleTimeSeconds; + return (this); + } + + + + /******************************************************************************* + ** Getter for maxIdleTimeExcessConnectionsSeconds + *******************************************************************************/ + public Integer getMaxIdleTimeExcessConnectionsSeconds() + { + return (this.maxIdleTimeExcessConnectionsSeconds); + } + + + + /******************************************************************************* + ** Setter for maxIdleTimeExcessConnectionsSeconds + *******************************************************************************/ + public void setMaxIdleTimeExcessConnectionsSeconds(Integer maxIdleTimeExcessConnectionsSeconds) + { + this.maxIdleTimeExcessConnectionsSeconds = maxIdleTimeExcessConnectionsSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for maxIdleTimeExcessConnectionsSeconds + *******************************************************************************/ + public ConnectionPoolSettings withMaxIdleTimeExcessConnectionsSeconds(Integer maxIdleTimeExcessConnectionsSeconds) + { + this.maxIdleTimeExcessConnectionsSeconds = maxIdleTimeExcessConnectionsSeconds; + return (this); + } + + + + /******************************************************************************* + ** Getter for testConnectionOnCheckout + *******************************************************************************/ + public Boolean getTestConnectionOnCheckout() + { + return (this.testConnectionOnCheckout); + } + + + + /******************************************************************************* + ** Setter for testConnectionOnCheckout + *******************************************************************************/ + public void setTestConnectionOnCheckout(Boolean testConnectionOnCheckout) + { + this.testConnectionOnCheckout = testConnectionOnCheckout; + } + + + + /******************************************************************************* + ** Fluent setter for testConnectionOnCheckout + *******************************************************************************/ + public ConnectionPoolSettings withTestConnectionOnCheckout(Boolean testConnectionOnCheckout) + { + this.testConnectionOnCheckout = testConnectionOnCheckout; + return (this); + } + + + + /******************************************************************************* + ** Getter for checkoutTimeoutSeconds + *******************************************************************************/ + public Integer getCheckoutTimeoutSeconds() + { + return (this.checkoutTimeoutSeconds); + } + + + + /******************************************************************************* + ** Setter for checkoutTimeoutSeconds + *******************************************************************************/ + public void setCheckoutTimeoutSeconds(Integer checkoutTimeoutSeconds) + { + this.checkoutTimeoutSeconds = checkoutTimeoutSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for checkoutTimeoutSeconds + *******************************************************************************/ + public ConnectionPoolSettings withCheckoutTimeoutSeconds(Integer checkoutTimeoutSeconds) + { + this.checkoutTimeoutSeconds = checkoutTimeoutSeconds; + return (this); + } + +} diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSBackendMetaData.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSBackendMetaData.java index a86a6f45..aa93d363 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSBackendMetaData.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSBackendMetaData.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.module.rdbms.model.metadata; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule; @@ -42,6 +43,12 @@ public class RDBMSBackendMetaData extends QBackendMetaData private String jdbcUrl; private String jdbcDriverClassName; + private QCodeReference connectionProvider; + + private ConnectionPoolSettings connectionPoolSettings; + + private RDBMSBackendMetaData readOnlyBackendMetaData; + /******************************************************************************* @@ -316,6 +323,7 @@ public class RDBMSBackendMetaData extends QBackendMetaData } + /******************************************************************************* ** Getter for jdbcDriverClassName *******************************************************************************/ @@ -346,4 +354,96 @@ public class RDBMSBackendMetaData extends QBackendMetaData } + + /******************************************************************************* + ** Getter for connectionProvider + *******************************************************************************/ + public QCodeReference getConnectionProvider() + { + return (this.connectionProvider); + } + + + + /******************************************************************************* + ** Setter for connectionProvider + *******************************************************************************/ + public void setConnectionProvider(QCodeReference connectionProvider) + { + this.connectionProvider = connectionProvider; + } + + + + /******************************************************************************* + ** Fluent setter for connectionProvider + *******************************************************************************/ + public RDBMSBackendMetaData withConnectionProvider(QCodeReference connectionProvider) + { + this.connectionProvider = connectionProvider; + return (this); + } + + + + /******************************************************************************* + ** Getter for readOnlyBackendMetaData + *******************************************************************************/ + public RDBMSBackendMetaData getReadOnlyBackendMetaData() + { + return (this.readOnlyBackendMetaData); + } + + + + /******************************************************************************* + ** Setter for readOnlyBackendMetaData + *******************************************************************************/ + public void setReadOnlyBackendMetaData(RDBMSBackendMetaData readOnlyBackendMetaData) + { + this.readOnlyBackendMetaData = readOnlyBackendMetaData; + } + + + + /******************************************************************************* + ** Fluent setter for readOnlyBackendMetaData + *******************************************************************************/ + public RDBMSBackendMetaData withReadOnlyBackendMetaData(RDBMSBackendMetaData readOnlyBackendMetaData) + { + this.readOnlyBackendMetaData = readOnlyBackendMetaData; + return (this); + } + + + + /******************************************************************************* + ** Getter for connectionPoolSettings + *******************************************************************************/ + public ConnectionPoolSettings getConnectionPoolSettings() + { + return (this.connectionPoolSettings); + } + + + + /******************************************************************************* + ** Setter for connectionPoolSettings + *******************************************************************************/ + public void setConnectionPoolSettings(ConnectionPoolSettings connectionPoolSettings) + { + this.connectionPoolSettings = connectionPoolSettings; + } + + + + /******************************************************************************* + ** Fluent setter for connectionPoolSettings + *******************************************************************************/ + public RDBMSBackendMetaData withConnectionPoolSettings(ConnectionPoolSettings connectionPoolSettings) + { + this.connectionPoolSettings = connectionPoolSettings; + return (this); + } + } diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProviderTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProviderTest.java new file mode 100644 index 00000000..23b0e7e0 --- /dev/null +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProviderTest.java @@ -0,0 +1,237 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.rdbms.jdbc; + + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; +import com.kingsrook.qqq.backend.module.rdbms.BaseTest; +import com.kingsrook.qqq.backend.module.rdbms.TestUtils; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.ConnectionPoolSettings; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; +import com.mchange.v2.resourcepool.TimeoutException; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +/******************************************************************************* + ** Unit test for C3P0PooledConnectionProvider + *******************************************************************************/ +class C3P0PooledConnectionProviderTest extends BaseTest +{ + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() throws Exception + { + TestUtils.primeTestDatabase("prime-test-database.sql"); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // must call this after the primeTestDatabase call (as i uses a raw version of the backend, w/o our updated settings) // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ConnectionManager.resetConnectionProviders(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @AfterEach + void afterEach() + { + //////////////////////////////////////////////////////////// + // just for good measure, do this after each test in here // + //////////////////////////////////////////////////////////// + ConnectionManager.resetConnectionProviders(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + // @RepeatedTest(100) + void test() throws Exception + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // change the default database backend to use the class under test here - the C3PL connection pool provider // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + QInstance qInstance = TestUtils.defineInstance(); + RDBMSBackendMetaData backend = (RDBMSBackendMetaData) qInstance.getBackend(TestUtils.DEFAULT_BACKEND_NAME); + backend.setConnectionProvider(new QCodeReference(C3P0PooledConnectionProvider.class)); + QContext.init(qInstance, new QSession()); + + for(int i = 0; i < 5; i++) + { + new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON)); + } + + JSONObject debugValues = getDebugStateValues(true); + assertThat(debugValues.getInt("numConnections")).isEqualTo(3); // one time (in a @RepeatedTest(100) we saw a 3 != 6 here...) + + //////////////////////////////////////////////////////////////////// + // open up 4 transactions - confirm the pool opens some new conns // + //////////////////////////////////////////////////////////////////// + List transactions = new ArrayList<>(); + for(int i = 0; i < 5; i++) + { + transactions.add(QBackendTransaction.openFor(new InsertInput(TestUtils.TABLE_NAME_PERSON))); + } + + debugValues = getDebugStateValues(true); + assertThat(debugValues.getInt("numConnections")).isGreaterThan(3); + + transactions.forEach(transaction -> transaction.close()); + + ///////////////////////////////////////////////////////////////////////// + // might take a second for the pool to re-claim the closed connections // + ///////////////////////////////////////////////////////////////////////// + boolean foundMatch = false; + for(int i = 0; i < 5; i++) + { + debugValues = getDebugStateValues(true); + if(debugValues.getInt("numConnections") == debugValues.getInt("numIdleConnections")) + { + foundMatch = true; + break; + } + System.out.println("oops!"); + SleepUtils.sleep(250, TimeUnit.MILLISECONDS); + } + + assertTrue(foundMatch, "The pool didn't re-claim all connections..."); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPoolSettings() throws Exception + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // change the default database backend to use the class under test here - the C3PL connection pool provider // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + QInstance qInstance = TestUtils.defineInstance(); + RDBMSBackendMetaData backend = (RDBMSBackendMetaData) qInstance.getBackend(TestUtils.DEFAULT_BACKEND_NAME); + backend.setConnectionProvider(new QCodeReference(C3P0PooledConnectionProvider.class)); + backend.setConnectionPoolSettings(new ConnectionPoolSettings() + .withInitialPoolSize(2) + .withAcquireIncrement(1) + .withMinPoolSize(1) + .withMaxPoolSize(4) + .withCheckoutTimeoutSeconds(1)); + QContext.init(qInstance, new QSession()); + + ///////////////////////// + // assert initial size // + ///////////////////////// + new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON)); + JSONObject debugValues = getDebugStateValues(true); + assertThat(debugValues.getInt("numConnections")).isEqualTo(2); + + /////////////////////////////////////////////////////////////////////// + // open (and close) 5 conns - shouldn't get bigger than initial size // + /////////////////////////////////////////////////////////////////////// + for(int i = 0; i < 5; i++) + { + new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON)); + } + debugValues = getDebugStateValues(true); + assertThat(debugValues.getInt("numConnections")).isEqualTo(2); // one time (in a @RepeatedTest(100) we saw a 3 != 6 here...) + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // open up 4 transactions - confirm the pool opens some new conns, but stops at the max, and throws based on checkoutTimeout setting // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + List transactions = new ArrayList<>(); + for(int i = 0; i < 5; i++) + { + if(i == 4) + { + ////////////////////////////////////////// + // expect this one to fail - full pool! // + ////////////////////////////////////////// + assertThatThrownBy(() -> QBackendTransaction.openFor(new InsertInput(TestUtils.TABLE_NAME_PERSON))) + .hasRootCauseInstanceOf(TimeoutException.class); + } + else + { + transactions.add(QBackendTransaction.openFor(new InsertInput(TestUtils.TABLE_NAME_PERSON))); + } + } + + debugValues = getDebugStateValues(true); + assertThat(debugValues.getInt("numConnections")).isEqualTo(4); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static JSONObject getDebugStateValues(boolean printIt) + { + JSONArray debugArray = ConnectionManager.dumpConnectionProviderDebug(); + for(int i = 0; i < debugArray.length(); i++) + { + JSONObject object = debugArray.getJSONObject(i); + if(TestUtils.DEFAULT_BACKEND_NAME.equals(object.optString("backendName"))) + { + JSONObject values = object.getJSONObject("values"); + if(printIt) + { + System.out.println(values.toString(3)); + } + + JSONObject state = values.getJSONObject("state"); + return state; + } + } + + fail("Didn't find debug values..."); + return (null); + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index ba87ccf1..a6afe0ca 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -82,6 +82,7 @@ import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportDestination; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; @@ -1195,6 +1196,7 @@ public class QJavalinImplementation countInput.setTimeoutSeconds(DEFAULT_COUNT_TIMEOUT_SECONDS); countInput.setQueryJoins(processQueryJoinsParam(context)); countInput.setIncludeDistinctCount(QJavalinUtils.queryParamIsTrue(context, "includeDistinct")); + countInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); CountAction countAction = new CountAction(); CountOutput countOutput = countAction.execute(countInput); @@ -1250,6 +1252,7 @@ public class QJavalinImplementation queryInput.setShouldGenerateDisplayValues(true); queryInput.setShouldTranslatePossibleValues(true); queryInput.setTimeoutSeconds(DEFAULT_QUERY_TIMEOUT_SECONDS); + queryInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ);