Merge pull request #31 from Kingsrook/feature/query-timeout-and-cancel

Feature/query timeout and cancel
This commit is contained in:
2023-07-25 08:14:09 -05:00
committed by GitHub
14 changed files with 541 additions and 20 deletions

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,73 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.rdbms.actions;
import java.sql.Statement;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Helper to cancel statements that timeout.
*******************************************************************************/
public class StatementTimeoutCanceller implements Runnable
{
private static final QLogger LOG = QLogger.getLogger(StatementTimeoutCanceller.class);
private final Statement statement;
private final String sql;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public StatementTimeoutCanceller(Statement statement, CharSequence sql)
{
this.statement = statement;
this.sql = sql.toString();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run()
{
try
{
statement.cancel();
LOG.info("Cancelled timed out statement", logPair("sql", sql));
}
catch(Exception e)
{
LOG.warn("Error trying to cancel statement after timeout", e, logPair("sql", sql));
}
throw (new QRuntimeException("Statement timed out and was cancelled."));
}
}