From b8f946947799c34de09658f99c7e66d8ae2767be Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 12 Feb 2024 18:51:34 -0600 Subject: [PATCH] CE-847 New process to "heal" records w/ an unhealthy automation status (failed or leaked-running) back to pending. --- .../actions/automation/AutomationStatus.java | 35 +- ...adRecordAutomationStatusesProcessStep.java | 298 ++++++++++++++++++ .../RunTableAutomationsProcessStep.java | 11 +- ...cordAutomationStatusesProcessStepTest.java | 204 ++++++++++++ 4 files changed, 536 insertions(+), 12 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStep.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStepTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/AutomationStatus.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/AutomationStatus.java index 49679fe1..14635b62 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/AutomationStatus.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/AutomationStatus.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.actions.automation; +import java.util.Objects; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum; @@ -55,6 +56,30 @@ public enum AutomationStatus implements PossibleValueEnum + /******************************************************************************* + ** 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 ** @@ -106,10 +131,10 @@ public enum AutomationStatus implements PossibleValueEnum public String getInsertOrUpdate() { return switch(this) - { - case PENDING_INSERT_AUTOMATIONS, RUNNING_INSERT_AUTOMATIONS, FAILED_INSERT_AUTOMATIONS -> "Insert"; - case PENDING_UPDATE_AUTOMATIONS, RUNNING_UPDATE_AUTOMATIONS, FAILED_UPDATE_AUTOMATIONS -> "Update"; - case OK -> ""; - }; + { + case PENDING_INSERT_AUTOMATIONS, RUNNING_INSERT_AUTOMATIONS, FAILED_INSERT_AUTOMATIONS -> "Insert"; + case PENDING_UPDATE_AUTOMATIONS, RUNNING_UPDATE_AUTOMATIONS, FAILED_UPDATE_AUTOMATIONS -> "Update"; + case OK -> ""; + }; } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStep.java new file mode 100644 index 00000000..92d3b3a3 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStep.java @@ -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 . + */ + +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 +{ + public static final String NAME = "HealBadRecordAutomationStatusesProcess"; + + private static final QLogger LOG = QLogger.getLogger(HealBadRecordAutomationStatusesProcessStep.class); + + private static final Map 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("Warning:")) + .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(""" +
    + #foreach($string in $warnings) +
  • $string
  • + #end +
+ """))) + + .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 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 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 recordsToUpdate = new ArrayList<>(); + Map 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 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; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java index 29f1c2f1..1c02a563 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java @@ -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.QFrontendComponentMetaData; 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.tables.TablesPossibleValueSourceMetaDataProvider; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -75,18 +75,15 @@ public class RunTableAutomationsProcessStep implements BackendStep, MetaDataProd new QFrontendStepMetaData() .withName("input") .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)), new QBackendStepMetaData() .withName("run") - .withCode(new QCodeReference(getClass())) - .withInputData(new QFunctionInputMetaData() - .withField(new QFieldMetaData("tableName", QFieldType.STRING)) - .withField(new QFieldMetaData("automationProviderName", QFieldType.STRING))), + .withCode(new QCodeReference(getClass())), new QFrontendStepMetaData() .withName("output") .withComponent(new QFrontendComponentMetaData().withType(QComponentType.VIEW_FORM)) - .withFormField(new QFieldMetaData("ok", QFieldType.STRING)) + .withViewField(new QFieldMetaData("ok", QFieldType.STRING)) )); return (processMetaData); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStepTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStepTest.java new file mode 100644 index 00000000..b6124e69 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/HealBadRecordAutomationStatusesProcessStepTest.java @@ -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 . + */ + +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 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 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 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 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; + } + +} \ No newline at end of file