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)

This commit is contained in:
2023-07-20 20:10:03 -05:00
parent c53f5e935d
commit 0ff98ce7ea
13 changed files with 465 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();
}
}