mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
CE-847 New process to "heal" records w/ an unhealthy automation status (failed or leaked-running) back to pending.
This commit is contained in:
@ -22,6 +22,7 @@
|
|||||||
package com.kingsrook.qqq.backend.core.actions.automation;
|
package com.kingsrook.qqq.backend.core.actions.automation;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
|
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
|
||||||
|
|
||||||
|
|
||||||
@ -55,6 +56,30 @@ public enum AutomationStatus implements PossibleValueEnum<Integer>
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Get instance by id
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static AutomationStatus getById(Integer id)
|
||||||
|
{
|
||||||
|
if(id == null)
|
||||||
|
{
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(AutomationStatus value : AutomationStatus.values())
|
||||||
|
{
|
||||||
|
if(Objects.equals(value.id, id))
|
||||||
|
{
|
||||||
|
return (value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Getter for id
|
** Getter for id
|
||||||
**
|
**
|
||||||
@ -106,10 +131,10 @@ public enum AutomationStatus implements PossibleValueEnum<Integer>
|
|||||||
public String getInsertOrUpdate()
|
public String getInsertOrUpdate()
|
||||||
{
|
{
|
||||||
return switch(this)
|
return switch(this)
|
||||||
{
|
{
|
||||||
case PENDING_INSERT_AUTOMATIONS, RUNNING_INSERT_AUTOMATIONS, FAILED_INSERT_AUTOMATIONS -> "Insert";
|
case PENDING_INSERT_AUTOMATIONS, RUNNING_INSERT_AUTOMATIONS, FAILED_INSERT_AUTOMATIONS -> "Insert";
|
||||||
case PENDING_UPDATE_AUTOMATIONS, RUNNING_UPDATE_AUTOMATIONS, FAILED_UPDATE_AUTOMATIONS -> "Update";
|
case PENDING_UPDATE_AUTOMATIONS, RUNNING_UPDATE_AUTOMATIONS, FAILED_UPDATE_AUTOMATIONS -> "Update";
|
||||||
case OK -> "";
|
case OK -> "";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,298 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.processes.implementations.automation;
|
||||||
|
|
||||||
|
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||||
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||||
|
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.QueryOutput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.NowWithOffset;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
|
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.metadata.dashboard.nocode.HtmlWrapper;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.collections.MultiLevelMapHelper;
|
||||||
|
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Process to find records with a bad automation status, and repair them.
|
||||||
|
**
|
||||||
|
** Bad status are defined as:
|
||||||
|
** - failed insert or updates.
|
||||||
|
** - running insert or updates for more than X minutes (see input field value).
|
||||||
|
**
|
||||||
|
** Repair in this case means resetting their status to the corresponding (e.g.,
|
||||||
|
** insert/update) pending status.
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public class HealBadRecordAutomationStatusesProcessStep implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||||
|
{
|
||||||
|
public static final String NAME = "HealBadRecordAutomationStatusesProcess";
|
||||||
|
|
||||||
|
private static final QLogger LOG = QLogger.getLogger(HealBadRecordAutomationStatusesProcessStep.class);
|
||||||
|
|
||||||
|
private static final Map<Integer, Integer> statusUpdateMap = Map.of(
|
||||||
|
AutomationStatus.FAILED_INSERT_AUTOMATIONS.getId(), AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(),
|
||||||
|
AutomationStatus.RUNNING_INSERT_AUTOMATIONS.getId(), AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(),
|
||||||
|
AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId(),
|
||||||
|
AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||||
|
{
|
||||||
|
QProcessMetaData processMetaData = new QProcessMetaData()
|
||||||
|
.withName(NAME)
|
||||||
|
.withStepList(List.of(
|
||||||
|
new QFrontendStepMetaData()
|
||||||
|
.withName("input")
|
||||||
|
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
|
||||||
|
.withFormField(new QFieldMetaData("tableName", QFieldType.STRING).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME))
|
||||||
|
.withFormField(new QFieldMetaData("minutesOldLimit", QFieldType.INTEGER).withDefaultValue(60)),
|
||||||
|
new QBackendStepMetaData()
|
||||||
|
.withName("run")
|
||||||
|
.withCode(new QCodeReference(getClass())),
|
||||||
|
new QFrontendStepMetaData()
|
||||||
|
.withName("output")
|
||||||
|
|
||||||
|
.withComponent(new NoCodeWidgetFrontendComponentMetaData()
|
||||||
|
.withOutput(new WidgetHtmlLine()
|
||||||
|
.withCondition(new QFilterCriteria("warningCount", QCriteriaOperator.GREATER_THAN, 0))
|
||||||
|
.withWrapper(HtmlWrapper.divWithStyles(HtmlWrapper.STYLE_YELLOW))
|
||||||
|
.withVelocityTemplate("<b>Warning:</b>"))
|
||||||
|
.withOutput(new WidgetHtmlLine()
|
||||||
|
.withCondition(new QFilterCriteria("warningCount", QCriteriaOperator.GREATER_THAN, 0))
|
||||||
|
.withWrapper(HtmlWrapper.divWithStyles(HtmlWrapper.STYLE_INDENT_1))
|
||||||
|
.withWrapper(HtmlWrapper.divWithStyles(HtmlWrapper.STYLE_YELLOW))
|
||||||
|
.withVelocityTemplate("""
|
||||||
|
<ul>
|
||||||
|
#foreach($string in $warnings)
|
||||||
|
<li>$string</li>
|
||||||
|
#end
|
||||||
|
</ul>
|
||||||
|
""")))
|
||||||
|
|
||||||
|
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.VIEW_FORM))
|
||||||
|
.withViewField(new QFieldMetaData("totalRecordsUpdated", QFieldType.INTEGER) /* todo - didn't display commas... .withDisplayFormat(DisplayFormat.COMMAS) */)
|
||||||
|
|
||||||
|
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.RECORD_LIST))
|
||||||
|
.withRecordListField(new QFieldMetaData("tableName", QFieldType.STRING))
|
||||||
|
.withRecordListField(new QFieldMetaData("badStatus", QFieldType.STRING))
|
||||||
|
.withRecordListField(new QFieldMetaData("count", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS) /* todo - didn't display commas... */)
|
||||||
|
|
||||||
|
));
|
||||||
|
|
||||||
|
return (processMetaData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||||
|
{
|
||||||
|
int recordsUpdated = 0;
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
// if a table name is given, validate it, and run for just that table //
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
String tableName = runBackendStepInput.getValueString("tableName");
|
||||||
|
ArrayList<String> warnings = new ArrayList<>();
|
||||||
|
if(StringUtils.hasContent(tableName))
|
||||||
|
{
|
||||||
|
if(!QContext.getQInstance().getTables().containsKey(tableName))
|
||||||
|
{
|
||||||
|
throw (new QException("Unrecognized table name: " + tableName));
|
||||||
|
}
|
||||||
|
|
||||||
|
recordsUpdated += processTable(tableName, runBackendStepInput, runBackendStepOutput, warnings);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// else, try to run for all tables that have an automation status field //
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
for(QTableMetaData table : QContext.getQInstance().getTables().values())
|
||||||
|
{
|
||||||
|
recordsUpdated += processTable(table.getName(), runBackendStepInput, runBackendStepOutput, warnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runBackendStepOutput.addValue("totalRecordsUpdated", recordsUpdated);
|
||||||
|
runBackendStepOutput.addValue("warnings", warnings);
|
||||||
|
runBackendStepOutput.addValue("warningCount", warnings.size());
|
||||||
|
|
||||||
|
if(CollectionUtils.nullSafeIsEmpty(runBackendStepOutput.getRecords()))
|
||||||
|
{
|
||||||
|
runBackendStepOutput.addRecord(new QRecord()
|
||||||
|
.withValue("tableName", "--")
|
||||||
|
.withValue("badStatus", "--")
|
||||||
|
.withValue("count", "0"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private int processTable(String tableName, RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput, List<String> warnings)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Integer minutesOldLimit = Objects.requireNonNullElse(runBackendStepInput.getValueInteger("minutesOldLimit"), 60);
|
||||||
|
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// only process tables w/ automation details w/ a status tracking field //
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
if(table != null && table.getAutomationDetails() != null && table.getAutomationDetails().getStatusTracking() != null && StringUtils.hasContent(table.getAutomationDetails().getStatusTracking().getFieldName()))
|
||||||
|
{
|
||||||
|
String automationStatusFieldName = table.getAutomationDetails().getStatusTracking().getFieldName();
|
||||||
|
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// find the modify-date field on the table //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
String modifyDateFieldName = null;
|
||||||
|
for(QFieldMetaData field : table.getFields().values())
|
||||||
|
{
|
||||||
|
if(DynamicDefaultValueBehavior.MODIFY_DATE.equals(field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)))
|
||||||
|
{
|
||||||
|
modifyDateFieldName = field.getName();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(modifyDateFieldName == null)
|
||||||
|
{
|
||||||
|
warnings.add("Could not find a Modify Date field on table: " + tableName);
|
||||||
|
LOG.info("Couldn't find a MODIFY_DATE field on table", logPair("tableName", tableName));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
// query for records either FAILED, or RUNNING w/ modify date too old //
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
QueryInput queryInput = new QueryInput();
|
||||||
|
queryInput.setTableName(tableName);
|
||||||
|
queryInput.setFilter(new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR)
|
||||||
|
.withSubFilter(new QQueryFilter()
|
||||||
|
.withCriteria(new QFilterCriteria(automationStatusFieldName, QCriteriaOperator.IN, AutomationStatus.FAILED_INSERT_AUTOMATIONS.getId(), AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId())))
|
||||||
|
.withSubFilter(new QQueryFilter()
|
||||||
|
.withCriteria(new QFilterCriteria(automationStatusFieldName, QCriteriaOperator.IN, AutomationStatus.RUNNING_INSERT_AUTOMATIONS.getId(), AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId()))
|
||||||
|
.withCriteria(new QFilterCriteria(modifyDateFieldName, QCriteriaOperator.LESS_THAN, NowWithOffset.minus(minutesOldLimit, ChronoUnit.MINUTES))))
|
||||||
|
);
|
||||||
|
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// foreach record found, add it to list of records to be updated - mapping status to appropriate pending status //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
List<QRecord> recordsToUpdate = new ArrayList<>();
|
||||||
|
Map<String, Integer> countByStatus = new HashMap<>();
|
||||||
|
for(QRecord record : queryOutput.getRecords())
|
||||||
|
{
|
||||||
|
Integer badAutomationStatusId = record.getValueInteger(automationStatusFieldName);
|
||||||
|
Integer updateStatus = statusUpdateMap.get(badAutomationStatusId);
|
||||||
|
if(updateStatus != null)
|
||||||
|
{
|
||||||
|
AutomationStatus badStatus = AutomationStatus.getById(badAutomationStatusId);
|
||||||
|
if(badStatus != null)
|
||||||
|
{
|
||||||
|
MultiLevelMapHelper.getOrPutAndIncrement(countByStatus, badStatus.getLabel());
|
||||||
|
}
|
||||||
|
|
||||||
|
recordsToUpdate.add(new QRecord()
|
||||||
|
.withValue(table.getPrimaryKeyField(), record.getValue(table.getPrimaryKeyField()))
|
||||||
|
.withValue(automationStatusFieldName, updateStatus));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!recordsToUpdate.isEmpty())
|
||||||
|
{
|
||||||
|
LOG.info("Healing bad record automation statuses", logPair("tableName", tableName), logPair("count", recordsToUpdate.size()));
|
||||||
|
new UpdateAction().execute(new UpdateInput(tableName).withRecords(recordsToUpdate).withOmitTriggeringAutomations(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
for(Map.Entry<String, Integer> entry : countByStatus.entrySet())
|
||||||
|
{
|
||||||
|
runBackendStepOutput.addRecord(new QRecord()
|
||||||
|
.withValue("tableName", tableName)
|
||||||
|
.withValue("badStatus", entry.getKey())
|
||||||
|
.withValue("count", entry.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (recordsToUpdate.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
warnings.add("Error processing table: " + tableName + ": " + ExceptionUtils.getTopAndBottomMessages(e));
|
||||||
|
LOG.warn("Error processing table for bad automation statuses", e, logPair("tableName, name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -40,8 +40,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
|
|
||||||
@ -75,18 +75,15 @@ public class RunTableAutomationsProcessStep implements BackendStep, MetaDataProd
|
|||||||
new QFrontendStepMetaData()
|
new QFrontendStepMetaData()
|
||||||
.withName("input")
|
.withName("input")
|
||||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
|
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
|
||||||
.withFormField(new QFieldMetaData("tableName", QFieldType.STRING).withIsRequired(true))
|
.withFormField(new QFieldMetaData("tableName", QFieldType.STRING).withIsRequired(true).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME))
|
||||||
.withFormField(new QFieldMetaData("automationProviderName", QFieldType.STRING)),
|
.withFormField(new QFieldMetaData("automationProviderName", QFieldType.STRING)),
|
||||||
new QBackendStepMetaData()
|
new QBackendStepMetaData()
|
||||||
.withName("run")
|
.withName("run")
|
||||||
.withCode(new QCodeReference(getClass()))
|
.withCode(new QCodeReference(getClass())),
|
||||||
.withInputData(new QFunctionInputMetaData()
|
|
||||||
.withField(new QFieldMetaData("tableName", QFieldType.STRING))
|
|
||||||
.withField(new QFieldMetaData("automationProviderName", QFieldType.STRING))),
|
|
||||||
new QFrontendStepMetaData()
|
new QFrontendStepMetaData()
|
||||||
.withName("output")
|
.withName("output")
|
||||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.VIEW_FORM))
|
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.VIEW_FORM))
|
||||||
.withFormField(new QFieldMetaData("ok", QFieldType.STRING))
|
.withViewField(new QFieldMetaData("ok", QFieldType.STRING))
|
||||||
));
|
));
|
||||||
|
|
||||||
return (processMetaData);
|
return (processMetaData);
|
||||||
|
@ -0,0 +1,204 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.processes.implementations.automation;
|
||||||
|
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||||
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
|
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.processes.RunBackendStepOutput;
|
||||||
|
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.data.QRecord;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for HealBadRecordAutomationStatusesProcessStep
|
||||||
|
*******************************************************************************/
|
||||||
|
class HealBadRecordAutomationStatusesProcessStepTest extends BaseTest
|
||||||
|
{
|
||||||
|
private static String tableName = TestUtils.TABLE_NAME_PERSON_MEMORY;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testTwoFailedUpdates() throws QException
|
||||||
|
{
|
||||||
|
new InsertAction().execute(new InsertInput(tableName).withRecords(List.of(new QRecord(), new QRecord())));
|
||||||
|
List<QRecord> records = queryAllRecords();
|
||||||
|
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(QContext.getQInstance().getTable(tableName), records, AutomationStatus.FAILED_UPDATE_AUTOMATIONS, null);
|
||||||
|
|
||||||
|
assertThat(queryAllRecords()).allMatch(r -> AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||||
|
|
||||||
|
RunBackendStepOutput output = runProcessStep();
|
||||||
|
|
||||||
|
assertEquals(2, output.getValueInteger("totalRecordsUpdated"));
|
||||||
|
assertThat(queryAllRecords()).allMatch(r -> AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testOneFailedUpdateOneFailedInsert() throws QException
|
||||||
|
{
|
||||||
|
new InsertAction().execute(new InsertInput(tableName).withRecords(List.of(new QRecord(), new QRecord())));
|
||||||
|
List<QRecord> records = queryAllRecords();
|
||||||
|
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(QContext.getQInstance().getTable(tableName), records.subList(0, 1), AutomationStatus.FAILED_UPDATE_AUTOMATIONS, null);
|
||||||
|
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(QContext.getQInstance().getTable(tableName), records.subList(1, 2), AutomationStatus.FAILED_INSERT_AUTOMATIONS, null);
|
||||||
|
|
||||||
|
assertThat(queryAllRecords())
|
||||||
|
.anyMatch(r -> AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)))
|
||||||
|
.anyMatch(r -> AutomationStatus.FAILED_INSERT_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||||
|
|
||||||
|
RunBackendStepOutput output = runProcessStep();
|
||||||
|
|
||||||
|
assertEquals(2, output.getValueInteger("totalRecordsUpdated"));
|
||||||
|
assertThat(queryAllRecords())
|
||||||
|
.anyMatch(r -> AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)))
|
||||||
|
.anyMatch(r -> AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testOldRunning() throws QException
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
// temporarily remove the modify-date behavior //
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
QContext.getQInstance().getTable(tableName).getField("modifyDate").withBehavior(DynamicDefaultValueBehavior.NONE);
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// insert 2 records, one with an old modifyDate, one with 6 minutes ago //
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
new InsertAction().execute(new InsertInput(tableName).withRecords(List.of(
|
||||||
|
new QRecord().withValue("firstName", "Darin").withValue("modifyDate", Instant.parse("2023-01-01T12:00:00Z")),
|
||||||
|
new QRecord().withValue("firstName", "Tim").withValue("modifyDate", Instant.now().minus(6, ChronoUnit.MINUTES))
|
||||||
|
)));
|
||||||
|
List<QRecord> records = queryAllRecords();
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
// put those records both in status: running-updates //
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(QContext.getQInstance().getTable(tableName), records, AutomationStatus.RUNNING_UPDATE_AUTOMATIONS, null);
|
||||||
|
|
||||||
|
assertThat(queryAllRecords())
|
||||||
|
.allMatch(r -> AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||||
|
|
||||||
|
/////////////////////////////////////
|
||||||
|
// restore the modifyDate behavior //
|
||||||
|
/////////////////////////////////////
|
||||||
|
QContext.getQInstance().getTable(tableName).getField("modifyDate").withBehavior(DynamicDefaultValueBehavior.MODIFY_DATE);
|
||||||
|
|
||||||
|
/////////////////////////
|
||||||
|
// run code under test //
|
||||||
|
/////////////////////////
|
||||||
|
RunBackendStepOutput output = runProcessStep();
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// assert we updated 1 (the old one) to pending-updates, the other left as running-updates //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
assertEquals(1, output.getValueInteger("totalRecordsUpdated"));
|
||||||
|
assertThat(queryAllRecords())
|
||||||
|
.anyMatch(r -> AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)))
|
||||||
|
.anyMatch(r -> AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||||
|
|
||||||
|
/////////////////////////////////
|
||||||
|
// re-run, with 3-minute limit //
|
||||||
|
/////////////////////////////////
|
||||||
|
output = runProcessStep(new RunBackendStepInput().withValues(Map.of("minutesOldLimit", 3)));
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
// assert that one updated too, and all are now pending-update //
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
assertEquals(1, output.getValueInteger("totalRecordsUpdated"));
|
||||||
|
assertThat(queryAllRecords())
|
||||||
|
.allMatch(r -> AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static Integer getAutomationStatus(QRecord r)
|
||||||
|
{
|
||||||
|
return r.getValueInteger(TestUtils.standardQqqAutomationStatusField().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static List<QRecord> queryAllRecords() throws QException
|
||||||
|
{
|
||||||
|
return new QueryAction().execute(new QueryInput(tableName)).getRecords();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static RunBackendStepOutput runProcessStep() throws QException
|
||||||
|
{
|
||||||
|
RunBackendStepInput input = new RunBackendStepInput();
|
||||||
|
return runProcessStep(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static RunBackendStepOutput runProcessStep(RunBackendStepInput input) throws QException
|
||||||
|
{
|
||||||
|
RunBackendStepOutput output = new RunBackendStepOutput();
|
||||||
|
new HealBadRecordAutomationStatusesProcessStep().run(input, output);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user