From 0ff98ce7ea1b06bf386181e116f8a2504bb3f438 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 20 Jul 2023 20:10:03 -0500 Subject: [PATCH] Add internal timeouts to RDBMS query, count, and aggregate, with timeoutSeconds field on their inputs; also add cancel method on those 3 actions, implemented down in RDBMS as well (e.g., to cancel inresponse to http request being abandoned) --- .../interfaces/BaseQueryInterface.java | 11 ++ .../core/actions/tables/AggregateAction.java | 25 +++- .../core/actions/tables/CountAction.java | 25 +++- .../core/actions/tables/QueryAction.java | 19 ++- .../tables/helpers/ActionTimeoutHelper.java | 109 ++++++++++++++++++ .../tables/aggregate/AggregateInput.java | 33 ++++++ .../actions/tables/count/CountInput.java | 33 ++++++ .../actions/tables/query/QueryInput.java | 32 +++++ .../rdbms/actions/AbstractRDBMSAction.java | 28 +++++ .../rdbms/actions/RDBMSAggregateAction.java | 51 +++++++- .../rdbms/actions/RDBMSCountAction.java | 52 ++++++++- .../rdbms/actions/RDBMSQueryAction.java | 65 ++++++++--- .../javalin/QJavalinImplementation.java | 2 + 13 files changed, 465 insertions(+), 20 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ActionTimeoutHelper.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/BaseQueryInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/BaseQueryInterface.java index f02beada..9e244d3b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/BaseQueryInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/BaseQueryInterface.java @@ -67,4 +67,15 @@ public interface BaseQueryInterface } } + + /******************************************************************************* + ** + *******************************************************************************/ + default void cancelAction() + { + ////////////////////////////////////////////// + // initially at least, a noop in base class // + ////////////////////////////////////////////// + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateAction.java index 56ce9555..42ade1f5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateAction.java @@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface; import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; @@ -41,6 +42,12 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; *******************************************************************************/ public class AggregateAction { + private static final QLogger LOG = QLogger.getLogger(AggregateAction.class); + + private AggregateInterface aggregateInterface; + + + /******************************************************************************* ** *******************************************************************************/ @@ -56,7 +63,7 @@ public class AggregateAction QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(aggregateInput.getBackend()); - AggregateInterface aggregateInterface = qModule.getAggregateInterface(); + aggregateInterface = qModule.getAggregateInterface(); aggregateInterface.setQueryStat(queryStat); AggregateOutput aggregateOutput = aggregateInterface.execute(aggregateInput); @@ -64,4 +71,20 @@ public class AggregateAction return aggregateOutput; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void cancel() + { + if(aggregateInterface == null) + { + LOG.warn("aggregateInterface object was null when requested to cancel"); + return; + } + + aggregateInterface.cancelAction(); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/CountAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/CountAction.java index 92337d6f..394df6f2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/CountAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/CountAction.java @@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; 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.metadata.QBackendMetaData; @@ -41,6 +42,12 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; *******************************************************************************/ public class CountAction { + private static final QLogger LOG = QLogger.getLogger(CountAction.class); + + private CountInterface countInterface; + + + /******************************************************************************* ** *******************************************************************************/ @@ -56,7 +63,7 @@ public class CountAction QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(countInput.getBackend()); - CountInterface countInterface = qModule.getCountInterface(); + countInterface = qModule.getCountInterface(); countInterface.setQueryStat(queryStat); CountOutput countOutput = countInterface.execute(countInput); @@ -64,4 +71,20 @@ public class CountAction return countOutput; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void cancel() + { + if(countInterface == null) + { + LOG.warn("countInterface object was null when requested to cancel"); + return; + } + + countInterface.cancelAction(); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index 2b6bcafd..3834edfb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -76,6 +76,7 @@ public class QueryAction private Optional postQueryRecordCustomizer; private QueryInput queryInput; + private QueryInterface queryInterface; private QPossibleValueTranslator qPossibleValueTranslator; @@ -121,7 +122,7 @@ public class QueryAction QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend); - QueryInterface queryInterface = qModule.getQueryInterface(); + queryInterface = qModule.getQueryInterface(); queryInterface.setQueryStat(queryStat); QueryOutput queryOutput = queryInterface.execute(queryInput); @@ -339,4 +340,20 @@ public class QueryAction } } } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void cancel() + { + if(queryInterface == null) + { + LOG.warn("queryInterface object was null when requested to cancel"); + return; + } + + queryInterface.cancelAction(); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ActionTimeoutHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ActionTimeoutHelper.java new file mode 100644 index 00000000..853e8a2b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ActionTimeoutHelper.java @@ -0,0 +1,109 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.actions.tables.helpers; + + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + + +/******************************************************************************* + ** For actions that may want to set a timeout, and cancel themselves if they run + ** too long - this class helps. + ** + ** Construct with the timeout (delay & timeUnit), and a runnable that takes care + ** of doing the cancel (e.g., cancelling a JDBC statement). + ** + ** Call start() to make a future get scheduled (note, if delay was null or <= 0, + ** then it doesn't get scheduled at all). + ** + ** Call cancel() if the action got far enough/completed, to cancel the future. + ** + ** You can check didTimeout (getDidTimeout()) to know if the timeout did occur. + *******************************************************************************/ +public class ActionTimeoutHelper +{ + private final Integer delay; + private final TimeUnit timeUnit; + private final Runnable runnable; + private ScheduledFuture future; + + private boolean didTimeout = false; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ActionTimeoutHelper(Integer delay, TimeUnit timeUnit, Runnable runnable) + { + this.delay = delay; + this.timeUnit = timeUnit; + this.runnable = runnable; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void start() + { + if(delay == null || delay <= 0) + { + return; + } + + future = Executors.newSingleThreadScheduledExecutor().schedule(() -> + { + didTimeout = true; + runnable.run(); + }, delay, timeUnit); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void cancel() + { + if(future != null) + { + future.cancel(true); + } + } + + + + /******************************************************************************* + ** Getter for didTimeout + ** + *******************************************************************************/ + public boolean getDidTimeout() + { + return didTimeout; + } + +} 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 04459f04..862ebcf6 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 @@ -40,6 +40,8 @@ public class AggregateInput extends AbstractTableActionInput private List groupBys = new ArrayList<>(); private Integer limit; + private Integer timeoutSeconds; + private List queryJoins = null; @@ -269,4 +271,35 @@ public class AggregateInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for timeoutSeconds + *******************************************************************************/ + public Integer getTimeoutSeconds() + { + return (this.timeoutSeconds); + } + + + + /******************************************************************************* + ** Setter for timeoutSeconds + *******************************************************************************/ + public void setTimeoutSeconds(Integer timeoutSeconds) + { + this.timeoutSeconds = timeoutSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for timeoutSeconds + *******************************************************************************/ + public AggregateInput withTimeoutSeconds(Integer timeoutSeconds) + { + this.timeoutSeconds = timeoutSeconds; + return (this); + } + } 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 b6383d99..e1994d8b 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 @@ -37,6 +37,8 @@ public class CountInput extends AbstractTableActionInput { private QQueryFilter filter; + private Integer timeoutSeconds; + private List queryJoins = null; private Boolean includeDistinctCount = false; @@ -174,4 +176,35 @@ public class CountInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for timeoutSeconds + *******************************************************************************/ + public Integer getTimeoutSeconds() + { + return (this.timeoutSeconds); + } + + + + /******************************************************************************* + ** Setter for timeoutSeconds + *******************************************************************************/ + public void setTimeoutSeconds(Integer timeoutSeconds) + { + this.timeoutSeconds = timeoutSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for timeoutSeconds + *******************************************************************************/ + public CountInput withTimeoutSeconds(Integer timeoutSeconds) + { + this.timeoutSeconds = timeoutSeconds; + return (this); + } + } 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 d422d601..0c9c24fb 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 @@ -42,6 +42,7 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn private QQueryFilter filter; private RecordPipe recordPipe; + private Integer timeoutSeconds; private boolean shouldTranslatePossibleValues = false; private boolean shouldGenerateDisplayValues = false; @@ -537,4 +538,35 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn return (this); } + + + /******************************************************************************* + ** Getter for timeoutSeconds + *******************************************************************************/ + public Integer getTimeoutSeconds() + { + return (this.timeoutSeconds); + } + + + + /******************************************************************************* + ** Setter for timeoutSeconds + *******************************************************************************/ + public void setTimeoutSeconds(Integer timeoutSeconds) + { + this.timeoutSeconds = timeoutSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for timeoutSeconds + *******************************************************************************/ + public QueryInput withTimeoutSeconds(Integer timeoutSeconds) + { + this.timeoutSeconds = timeoutSeconds; + return (this); + } + } 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 c0890aff..1301f2d5 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 @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.module.rdbms.actions; import java.io.Serializable; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.time.Instant; @@ -91,6 +92,9 @@ public abstract class AbstractRDBMSAction implements QActionInterface protected QueryStat queryStat; + protected PreparedStatement statement; + protected boolean isCancelled = false; + /******************************************************************************* @@ -1094,4 +1098,28 @@ public abstract class AbstractRDBMSAction implements QActionInterface this.queryStat = queryStat; } + + + /******************************************************************************* + ** + *******************************************************************************/ + protected void doCancelQuery() + { + isCancelled = true; + if(statement == null) + { + LOG.warn("Statement was null when requested to cancel query"); + return; + } + + try + { + statement.cancel(); + } + catch(SQLException e) + { + LOG.warn("Error trying to cancel query (statement)", e); + } + } + } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java index 641990e6..ce720120 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java @@ -27,8 +27,11 @@ import java.sql.Connection; import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface; +import com.kingsrook.qqq.backend.core.actions.tables.helpers.ActionTimeoutHelper; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput; @@ -53,6 +56,7 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega { private static final QLogger LOG = QLogger.getLogger(RDBMSAggregateAction.class); + private ActionTimeoutHelper actionTimeoutHelper; /******************************************************************************* @@ -102,8 +106,21 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega try(Connection connection = getConnection(aggregateInput)) { - QueryManager.executeStatement(connection, sql, ((ResultSet resultSet) -> + statement = connection.prepareStatement(sql); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // set up & start an actionTimeoutHelper (note, internally it'll deal with the time being null or negative as meaning not to timeout) // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + actionTimeoutHelper = new ActionTimeoutHelper(aggregateInput.getTimeoutSeconds(), TimeUnit.SECONDS, new StatementTimeoutCanceller(statement, sql)); + actionTimeoutHelper.start(); + + QueryManager.executeStatement(statement, ((ResultSet resultSet) -> { + ///////////////////////////////////////////////////////////////////////// + // once we've started getting results, go ahead and cancel the timeout // + ///////////////////////////////////////////////////////////////////////// + actionTimeoutHelper.cancel(); + while(resultSet.next()) { setQueryStatFirstResultTime(); @@ -156,9 +173,30 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega } catch(Exception e) { + if(actionTimeoutHelper != null && actionTimeoutHelper.getDidTimeout()) + { + setQueryStatFirstResultTime(); + throw (new QUserFacingException("Aggregate query timed out.")); + } + + if(isCancelled) + { + throw (new QUserFacingException("Aggregate query was cancelled.")); + } + LOG.warn("Error executing aggregate", e); throw new QException("Error executing aggregate", e); } + finally + { + if(actionTimeoutHelper != null) + { + ///////////////////////////////////////// + // make sure the timeout got cancelled // + ///////////////////////////////////////// + actionTimeoutHelper.cancel(); + } + } } @@ -199,4 +237,15 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega return (StringUtils.join(",", columns)); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void cancelAction() + { + doCancelQuery(); + } + } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java index e49cdc27..7177a4a1 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java @@ -27,8 +27,11 @@ import java.sql.Connection; import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; +import com.kingsrook.qqq.backend.core.actions.tables.helpers.ActionTimeoutHelper; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; @@ -46,6 +49,8 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf { private static final QLogger LOG = QLogger.getLogger(RDBMSCountAction.class); + private ActionTimeoutHelper actionTimeoutHelper; + /******************************************************************************* @@ -84,8 +89,21 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf { long mark = System.currentTimeMillis(); - QueryManager.executeStatement(connection, sql, ((ResultSet resultSet) -> + statement = connection.prepareStatement(sql); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // set up & start an actionTimeoutHelper (note, internally it'll deal with the time being null or negative as meaning not to timeout) // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + actionTimeoutHelper = new ActionTimeoutHelper(countInput.getTimeoutSeconds(), TimeUnit.SECONDS, new StatementTimeoutCanceller(statement, sql)); + actionTimeoutHelper.start(); + + QueryManager.executeStatement(statement, ((ResultSet resultSet) -> { + ///////////////////////////////////////////////////////////////////////// + // once we've started getting results, go ahead and cancel the timeout // + ///////////////////////////////////////////////////////////////////////// + actionTimeoutHelper.cancel(); + if(resultSet.next()) { rs.setCount(resultSet.getInt("record_count")); @@ -107,9 +125,41 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf } catch(Exception e) { + if(actionTimeoutHelper != null && actionTimeoutHelper.getDidTimeout()) + { + setQueryStatFirstResultTime(); + throw (new QUserFacingException("Count timed out.")); + } + + if(isCancelled) + { + throw (new QUserFacingException("Count was cancelled.")); + } + LOG.warn("Error executing count", e); throw new QException("Error executing count", e); } + finally + { + if(actionTimeoutHelper != null) + { + ///////////////////////////////////////// + // make sure the timeout got cancelled // + ///////////////////////////////////////// + actionTimeoutHelper.cancel(); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void cancelAction() + { + doCancelQuery(); } } 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 ea131832..4c934885 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 @@ -33,9 +33,12 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; +import com.kingsrook.qqq.backend.core.actions.tables.helpers.ActionTimeoutHelper; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; @@ -60,6 +63,8 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf { private static final QLogger LOG = QLogger.getLogger(RDBMSQueryAction.class); + private ActionTimeoutHelper actionTimeoutHelper; + /******************************************************************************* @@ -136,14 +141,29 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf try { + ///////////////////////////////////// + // create a statement from the SQL // + ///////////////////////////////////// + statement = createStatement(connection, sql.toString(), queryInput); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // set up & start an actionTimeoutHelper (note, internally it'll deal with the time being null or negative as meaning not to timeout) // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + actionTimeoutHelper = new ActionTimeoutHelper(queryInput.getTimeoutSeconds(), TimeUnit.SECONDS, new StatementTimeoutCanceller(statement, sql)); + actionTimeoutHelper.start(); + ////////////////////////////////////////////// // execute the query - iterate over results // ////////////////////////////////////////////// QueryOutput queryOutput = new QueryOutput(queryInput); - PreparedStatement statement = createStatement(connection, sql.toString(), queryInput); QueryManager.executeStatement(statement, ((ResultSet resultSet) -> { + ///////////////////////////////////////////////////////////////////////// + // once we've started getting results, go ahead and cancel the timeout // + ///////////////////////////////////////////////////////////////////////// + actionTimeoutHelper.cancel(); + ResultSetMetaData metaData = resultSet.getMetaData(); while(resultSet.next()) { @@ -201,6 +221,14 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf } finally { + if(actionTimeoutHelper != null) + { + ///////////////////////////////////////// + // make sure the timeout got cancelled // + ///////////////////////////////////////// + actionTimeoutHelper.cancel(); + } + if(needToCloseConnection) { connection.close(); @@ -209,6 +237,17 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf } catch(Exception e) { + if(actionTimeoutHelper != null && actionTimeoutHelper.getDidTimeout()) + { + setQueryStatFirstResultTime(); + throw (new QUserFacingException("Query timed out.")); + } + + if(isCancelled) + { + throw (new QUserFacingException("Query was cancelled.")); + } + LOG.warn("Error executing query", e); throw new QException("Error executing query", e); } @@ -282,20 +321,6 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf - /******************************************************************************* - ** - *******************************************************************************/ - private boolean filterOutHeavyFieldsIfNeeded(QFieldMetaData field, boolean shouldFetchHeavyFields) - { - if(!shouldFetchHeavyFields && field.getIsHeavy()) - { - return (false); - } - return (true); - } - - - /******************************************************************************* ** if we're not fetching heavy fields, instead just get their length. this ** method wraps the field 'sql name' (e.g., column_name or table_name.column_name) @@ -338,4 +363,14 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf return (statement); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void cancelAction() + { + doCancelQuery(); + } } 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 f61ad558..63c478bf 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 @@ -1075,6 +1075,7 @@ public class QJavalinImplementation countInput.setFilter(JsonUtils.toObject(filter, QQueryFilter.class)); } + countInput.setTimeoutSeconds(DEFAULT_COUNT_TIMEOUT_SECONDS); countInput.setQueryJoins(processQueryJoinsParam(context)); countInput.setIncludeDistinctCount(QJavalinUtils.queryParamIsTrue(context, "includeDistinct")); @@ -1131,6 +1132,7 @@ public class QJavalinImplementation queryInput.setTableName(table); queryInput.setShouldGenerateDisplayValues(true); queryInput.setShouldTranslatePossibleValues(true); + queryInput.setTimeoutSeconds(DEFAULT_QUERY_TIMEOUT_SECONDS); PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ);