mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
Merge pull request #31 from Kingsrook/feature/query-timeout-and-cancel
Feature/query timeout and cancel
This commit is contained in:
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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."));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user